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/__init__.py
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
import sys
|
2
|
+
from ..config import get_config_path, load_configs, add_config_entry, remove_config_entry
|
3
|
+
from .formatters import COLORS
|
4
|
+
|
5
|
+
def show_config_help():
|
6
|
+
"""Display help information about configuration."""
|
7
|
+
print(f"\n{COLORS['green']}{COLORS['bold']}Configuration Help:{COLORS['reset']}")
|
8
|
+
print(f" 1. {COLORS['cyan']}Create a config file at one of these locations:{COLORS['reset']}")
|
9
|
+
if sys.platform == "win32":
|
10
|
+
print(f" - {COLORS['yellow']}%APPDATA%\\ngpt\\ngpt.conf{COLORS['reset']}")
|
11
|
+
elif sys.platform == "darwin":
|
12
|
+
print(f" - {COLORS['yellow']}~/Library/Application Support/ngpt/ngpt.conf{COLORS['reset']}")
|
13
|
+
else:
|
14
|
+
print(f" - {COLORS['yellow']}~/.config/ngpt/ngpt.conf{COLORS['reset']}")
|
15
|
+
|
16
|
+
print(f" 2. {COLORS['cyan']}Format your config file as JSON:{COLORS['reset']}")
|
17
|
+
print(f"""{COLORS['yellow']} [
|
18
|
+
{{
|
19
|
+
"api_key": "your-api-key-here",
|
20
|
+
"base_url": "https://api.openai.com/v1/",
|
21
|
+
"provider": "OpenAI",
|
22
|
+
"model": "gpt-3.5-turbo"
|
23
|
+
}},
|
24
|
+
{{
|
25
|
+
"api_key": "your-second-api-key",
|
26
|
+
"base_url": "http://localhost:1337/v1/",
|
27
|
+
"provider": "Another Provider",
|
28
|
+
"model": "different-model"
|
29
|
+
}}
|
30
|
+
]{COLORS['reset']}""")
|
31
|
+
|
32
|
+
print(f" 3. {COLORS['cyan']}Or set environment variables:{COLORS['reset']}")
|
33
|
+
print(f" - {COLORS['yellow']}OPENAI_API_KEY{COLORS['reset']}")
|
34
|
+
print(f" - {COLORS['yellow']}OPENAI_BASE_URL{COLORS['reset']}")
|
35
|
+
print(f" - {COLORS['yellow']}OPENAI_MODEL{COLORS['reset']}")
|
36
|
+
|
37
|
+
print(f" 4. {COLORS['cyan']}Or provide command line arguments:{COLORS['reset']}")
|
38
|
+
print(f" {COLORS['yellow']}ngpt --api-key your-key --base-url https://api.example.com --model your-model \"Your prompt\"{COLORS['reset']}")
|
39
|
+
|
40
|
+
print(f" 5. {COLORS['cyan']}Use --config-index to specify which configuration to use or edit:{COLORS['reset']}")
|
41
|
+
print(f" {COLORS['yellow']}ngpt --config-index 1 \"Your prompt\"{COLORS['reset']}")
|
42
|
+
|
43
|
+
print(f" 6. {COLORS['cyan']}Use --provider to specify which configuration to use by provider name:{COLORS['reset']}")
|
44
|
+
print(f" {COLORS['yellow']}ngpt --provider Gemini \"Your prompt\"{COLORS['reset']}")
|
45
|
+
|
46
|
+
print(f" 7. {COLORS['cyan']}Use --config without arguments to add a new configuration:{COLORS['reset']}")
|
47
|
+
print(f" {COLORS['yellow']}ngpt --config{COLORS['reset']}")
|
48
|
+
print(f" Or specify an index or provider to edit an existing configuration:")
|
49
|
+
print(f" {COLORS['yellow']}ngpt --config --config-index 1{COLORS['reset']}")
|
50
|
+
print(f" {COLORS['yellow']}ngpt --config --provider Gemini{COLORS['reset']}")
|
51
|
+
|
52
|
+
print(f" 8. {COLORS['cyan']}Remove a configuration by index or provider:{COLORS['reset']}")
|
53
|
+
print(f" {COLORS['yellow']}ngpt --config --remove --config-index 1{COLORS['reset']}")
|
54
|
+
print(f" {COLORS['yellow']}ngpt --config --remove --provider Gemini{COLORS['reset']}")
|
55
|
+
|
56
|
+
print(f" 9. {COLORS['cyan']}List available models for the current configuration:{COLORS['reset']}")
|
57
|
+
print(f" {COLORS['yellow']}ngpt --list-models{COLORS['reset']}")
|
58
|
+
|
59
|
+
def check_config(config):
|
60
|
+
"""Check config for common issues and provide guidance."""
|
61
|
+
if not config.get("api_key"):
|
62
|
+
print(f"{COLORS['yellow']}{COLORS['bold']}Error: API key is not set.{COLORS['reset']}")
|
63
|
+
show_config_help()
|
64
|
+
return False
|
65
|
+
|
66
|
+
# Check for common URL mistakes
|
67
|
+
base_url = config.get("base_url", "")
|
68
|
+
if base_url and not (base_url.startswith("http://") or base_url.startswith("https://")):
|
69
|
+
print(f"{COLORS['yellow']}Warning: Base URL '{base_url}' doesn't start with http:// or https://{COLORS['reset']}")
|
70
|
+
|
71
|
+
return True
|
ngpt/cli/formatters.py
ADDED
@@ -0,0 +1,239 @@
|
|
1
|
+
import sys
|
2
|
+
import os
|
3
|
+
import shutil
|
4
|
+
import argparse
|
5
|
+
import re
|
6
|
+
import textwrap
|
7
|
+
import ctypes
|
8
|
+
|
9
|
+
# ANSI color codes for terminal output
|
10
|
+
COLORS = {
|
11
|
+
"reset": "\033[0m",
|
12
|
+
"bold": "\033[1m",
|
13
|
+
"cyan": "\033[36m",
|
14
|
+
"green": "\033[32m",
|
15
|
+
"yellow": "\033[33m",
|
16
|
+
"blue": "\033[34m",
|
17
|
+
"magenta": "\033[35m",
|
18
|
+
"gray": "\033[90m",
|
19
|
+
"bg_blue": "\033[44m",
|
20
|
+
"bg_cyan": "\033[46m"
|
21
|
+
}
|
22
|
+
|
23
|
+
# Check if ANSI colors are supported
|
24
|
+
def supports_ansi_colors():
|
25
|
+
"""Check if the current terminal supports ANSI colors."""
|
26
|
+
|
27
|
+
# If not a TTY, probably redirected, so no color
|
28
|
+
if not sys.stdout.isatty():
|
29
|
+
return False
|
30
|
+
|
31
|
+
# Windows specific checks
|
32
|
+
if sys.platform == "win32":
|
33
|
+
try:
|
34
|
+
# Windows 10+ supports ANSI colors in cmd/PowerShell
|
35
|
+
kernel32 = ctypes.windll.kernel32
|
36
|
+
|
37
|
+
# Try to enable ANSI color support
|
38
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
39
|
+
|
40
|
+
# Check if TERM_PROGRAM is set (WSL/ConEmu/etc.)
|
41
|
+
if os.environ.get('TERM_PROGRAM') or os.environ.get('WT_SESSION'):
|
42
|
+
return True
|
43
|
+
|
44
|
+
# Check Windows version - 10+ supports ANSI natively
|
45
|
+
winver = sys.getwindowsversion()
|
46
|
+
if winver.major >= 10:
|
47
|
+
return True
|
48
|
+
|
49
|
+
return False
|
50
|
+
except Exception:
|
51
|
+
return False
|
52
|
+
|
53
|
+
# Most UNIX systems support ANSI colors
|
54
|
+
return True
|
55
|
+
|
56
|
+
# Initialize color support
|
57
|
+
HAS_COLOR = supports_ansi_colors()
|
58
|
+
|
59
|
+
# If we're on Windows, use brighter colors that work better in PowerShell
|
60
|
+
if sys.platform == "win32" and HAS_COLOR:
|
61
|
+
COLORS["magenta"] = "\033[95m" # Bright magenta for metavars
|
62
|
+
COLORS["cyan"] = "\033[96m" # Bright cyan for options
|
63
|
+
|
64
|
+
# If no color support, use empty color codes
|
65
|
+
if not HAS_COLOR:
|
66
|
+
for key in COLORS:
|
67
|
+
COLORS[key] = ""
|
68
|
+
|
69
|
+
# Custom help formatter with color support
|
70
|
+
class ColoredHelpFormatter(argparse.HelpFormatter):
|
71
|
+
"""Help formatter that properly handles ANSI color codes without breaking alignment."""
|
72
|
+
|
73
|
+
def __init__(self, prog):
|
74
|
+
# Get terminal size for dynamic width adjustment
|
75
|
+
try:
|
76
|
+
self.term_width = shutil.get_terminal_size().columns
|
77
|
+
except:
|
78
|
+
self.term_width = 80 # Default if we can't detect terminal width
|
79
|
+
|
80
|
+
# Calculate dynamic layout values based on terminal width
|
81
|
+
self.formatter_width = self.term_width - 2 # Leave some margin
|
82
|
+
|
83
|
+
# For very wide terminals, limit the width to maintain readability
|
84
|
+
if self.formatter_width > 120:
|
85
|
+
self.formatter_width = 120
|
86
|
+
|
87
|
+
# Calculate help position based on terminal width (roughly 1/3 of width)
|
88
|
+
self.help_position = min(max(20, int(self.term_width * 0.33)), 36)
|
89
|
+
|
90
|
+
# Initialize the parent class with dynamic values
|
91
|
+
super().__init__(prog, max_help_position=self.help_position, width=self.formatter_width)
|
92
|
+
|
93
|
+
# Calculate wrap width based on remaining space after help position
|
94
|
+
self.wrap_width = self.formatter_width - self.help_position - 5
|
95
|
+
|
96
|
+
# Set up the text wrapper for help text
|
97
|
+
self.ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
98
|
+
self.wrapper = textwrap.TextWrapper(width=self.wrap_width)
|
99
|
+
|
100
|
+
def _strip_ansi(self, s):
|
101
|
+
"""Strip ANSI escape sequences for width calculations"""
|
102
|
+
return self.ansi_escape.sub('', s)
|
103
|
+
|
104
|
+
def _colorize(self, text, color, bold=False):
|
105
|
+
"""Helper to consistently apply color with optional bold"""
|
106
|
+
if bold:
|
107
|
+
return f"{COLORS['bold']}{COLORS[color]}{text}{COLORS['reset']}"
|
108
|
+
return f"{COLORS[color]}{text}{COLORS['reset']}"
|
109
|
+
|
110
|
+
def _format_action_invocation(self, action):
|
111
|
+
if not action.option_strings:
|
112
|
+
# For positional arguments
|
113
|
+
metavar = self._format_args(action, action.dest.upper())
|
114
|
+
return self._colorize(metavar, 'cyan', bold=True)
|
115
|
+
else:
|
116
|
+
# For optional arguments with different color for metavar
|
117
|
+
if action.nargs != argparse.SUPPRESS:
|
118
|
+
default = self._get_default_metavar_for_optional(action)
|
119
|
+
args_string = self._format_args(action, default)
|
120
|
+
|
121
|
+
# Color option name and metavar differently
|
122
|
+
option_part = ', '.join(action.option_strings)
|
123
|
+
colored_option = self._colorize(option_part, 'cyan', bold=True)
|
124
|
+
|
125
|
+
if args_string:
|
126
|
+
# Make metavars more visible with brackets and color
|
127
|
+
# If HAS_COLOR is False, brackets will help in PowerShell
|
128
|
+
if not HAS_COLOR:
|
129
|
+
# Add brackets to make metavars stand out even without color
|
130
|
+
formatted_args = f"<{args_string}>"
|
131
|
+
else:
|
132
|
+
# Use color for metavar
|
133
|
+
formatted_args = self._colorize(args_string, 'magenta')
|
134
|
+
|
135
|
+
return f"{colored_option} {formatted_args}"
|
136
|
+
else:
|
137
|
+
return colored_option
|
138
|
+
else:
|
139
|
+
return self._colorize(', '.join(action.option_strings), 'cyan', bold=True)
|
140
|
+
|
141
|
+
def _format_usage(self, usage, actions, groups, prefix):
|
142
|
+
usage_text = super()._format_usage(usage, actions, groups, prefix)
|
143
|
+
|
144
|
+
# Replace "usage:" with colored version
|
145
|
+
colored_usage = self._colorize("usage:", 'green', bold=True)
|
146
|
+
usage_text = usage_text.replace("usage:", colored_usage)
|
147
|
+
|
148
|
+
# We won't color metavars in usage text as it breaks the formatting
|
149
|
+
# Just return with the colored usage prefix
|
150
|
+
return usage_text
|
151
|
+
|
152
|
+
def _join_parts(self, part_strings):
|
153
|
+
"""Override to fix any potential formatting issues with section joins"""
|
154
|
+
return '\n'.join([part for part in part_strings if part])
|
155
|
+
|
156
|
+
def start_section(self, heading):
|
157
|
+
# Remove the colon as we'll add it with color
|
158
|
+
if heading.endswith(':'):
|
159
|
+
heading = heading[:-1]
|
160
|
+
heading_text = f"{self._colorize(heading, 'yellow', bold=True)}:"
|
161
|
+
super().start_section(heading_text)
|
162
|
+
|
163
|
+
def _get_help_string(self, action):
|
164
|
+
# Add color to help strings
|
165
|
+
help_text = action.help
|
166
|
+
if help_text:
|
167
|
+
return help_text.replace('(default:', f"{COLORS['gray']}(default:") + COLORS['reset']
|
168
|
+
return help_text
|
169
|
+
|
170
|
+
def _wrap_help_text(self, text, initial_indent="", subsequent_indent=" "):
|
171
|
+
"""Wrap long help text to prevent overflow"""
|
172
|
+
if not text:
|
173
|
+
return text
|
174
|
+
|
175
|
+
# Strip ANSI codes for width calculation
|
176
|
+
clean_text = self._strip_ansi(text)
|
177
|
+
|
178
|
+
# If the text is already short enough, return it as is
|
179
|
+
if len(clean_text) <= self.wrap_width:
|
180
|
+
return text
|
181
|
+
|
182
|
+
# Handle any existing ANSI codes
|
183
|
+
has_ansi = text != clean_text
|
184
|
+
wrap_text = clean_text
|
185
|
+
|
186
|
+
# Wrap the text
|
187
|
+
lines = self.wrapper.wrap(wrap_text)
|
188
|
+
|
189
|
+
# Add indentation to all but the first line
|
190
|
+
wrapped = lines[0]
|
191
|
+
for line in lines[1:]:
|
192
|
+
wrapped += f"\n{subsequent_indent}{line}"
|
193
|
+
|
194
|
+
# Re-add the ANSI codes if they were present
|
195
|
+
if has_ansi and text.endswith(COLORS['reset']):
|
196
|
+
wrapped += COLORS['reset']
|
197
|
+
|
198
|
+
return wrapped
|
199
|
+
|
200
|
+
def _format_action(self, action):
|
201
|
+
# For subparsers, just return the regular formatting
|
202
|
+
if isinstance(action, argparse._SubParsersAction):
|
203
|
+
return super()._format_action(action)
|
204
|
+
|
205
|
+
# Get the action header with colored parts (both option names and metavars)
|
206
|
+
# The coloring is now done in _format_action_invocation
|
207
|
+
action_header = self._format_action_invocation(action)
|
208
|
+
|
209
|
+
# Format help text
|
210
|
+
help_text = self._expand_help(action)
|
211
|
+
|
212
|
+
# Get the raw lengths without ANSI codes for formatting
|
213
|
+
raw_header_len = len(self._strip_ansi(action_header))
|
214
|
+
|
215
|
+
# Calculate the indent for the help text
|
216
|
+
help_position = min(self._action_max_length + 2, self._max_help_position)
|
217
|
+
help_indent = ' ' * help_position
|
218
|
+
|
219
|
+
# If the action header is too long, put help on the next line
|
220
|
+
if raw_header_len > help_position:
|
221
|
+
# An action header that's too long gets a line break
|
222
|
+
# Wrap the help text with proper indentation
|
223
|
+
wrapped_help = self._wrap_help_text(help_text, subsequent_indent=help_indent)
|
224
|
+
line = f"{action_header}\n{help_indent}{wrapped_help}"
|
225
|
+
else:
|
226
|
+
# Standard formatting with proper spacing
|
227
|
+
padding = ' ' * (help_position - raw_header_len)
|
228
|
+
# Wrap the help text with proper indentation
|
229
|
+
wrapped_help = self._wrap_help_text(help_text, subsequent_indent=help_indent)
|
230
|
+
line = f"{action_header}{padding}{wrapped_help}"
|
231
|
+
|
232
|
+
# Handle subactions
|
233
|
+
if action.help is argparse.SUPPRESS:
|
234
|
+
return line
|
235
|
+
|
236
|
+
if not action.help:
|
237
|
+
return line
|
238
|
+
|
239
|
+
return line
|
ngpt/cli/interactive.py
ADDED
@@ -0,0 +1,275 @@
|
|
1
|
+
import sys
|
2
|
+
import shutil
|
3
|
+
import datetime
|
4
|
+
import traceback
|
5
|
+
from .formatters import COLORS
|
6
|
+
from .renderers import prettify_markdown, prettify_streaming_markdown
|
7
|
+
|
8
|
+
# Optional imports for enhanced UI
|
9
|
+
try:
|
10
|
+
from prompt_toolkit import prompt as pt_prompt
|
11
|
+
from prompt_toolkit.styles import Style
|
12
|
+
from prompt_toolkit.key_binding import KeyBindings
|
13
|
+
from prompt_toolkit.formatted_text import HTML
|
14
|
+
from prompt_toolkit.history import InMemoryHistory
|
15
|
+
HAS_PROMPT_TOOLKIT = True
|
16
|
+
except ImportError:
|
17
|
+
HAS_PROMPT_TOOLKIT = False
|
18
|
+
|
19
|
+
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):
|
20
|
+
"""Start an interactive chat session with the AI.
|
21
|
+
|
22
|
+
Args:
|
23
|
+
client: The NGPTClient instance
|
24
|
+
web_search: Whether to enable web search capability
|
25
|
+
no_stream: Whether to disable streaming
|
26
|
+
temperature: Controls randomness in the response
|
27
|
+
top_p: Controls diversity via nucleus sampling
|
28
|
+
max_tokens: Maximum number of tokens to generate in each response
|
29
|
+
log_file: Optional filepath to log conversation to
|
30
|
+
preprompt: Custom system prompt to control AI behavior
|
31
|
+
prettify: Whether to enable markdown rendering
|
32
|
+
renderer: Which markdown renderer to use
|
33
|
+
stream_prettify: Whether to enable streaming with prettify
|
34
|
+
"""
|
35
|
+
# Get terminal width for better formatting
|
36
|
+
try:
|
37
|
+
term_width = shutil.get_terminal_size().columns
|
38
|
+
except:
|
39
|
+
term_width = 80 # Default fallback
|
40
|
+
|
41
|
+
# Improved visual header with better layout
|
42
|
+
header = f"{COLORS['cyan']}{COLORS['bold']}🤖 nGPT Interactive Chat Session 🤖{COLORS['reset']}"
|
43
|
+
print(f"\n{header}")
|
44
|
+
|
45
|
+
# Create a separator line - use a consistent separator length for all lines
|
46
|
+
separator_length = min(40, term_width - 10)
|
47
|
+
separator = f"{COLORS['gray']}{'─' * separator_length}{COLORS['reset']}"
|
48
|
+
print(separator)
|
49
|
+
|
50
|
+
# Group commands into categories with better formatting
|
51
|
+
print(f"\n{COLORS['cyan']}Navigation:{COLORS['reset']}")
|
52
|
+
print(f" {COLORS['yellow']}↑/↓{COLORS['reset']} : Browse input history")
|
53
|
+
|
54
|
+
print(f"\n{COLORS['cyan']}Session Commands:{COLORS['reset']}")
|
55
|
+
print(f" {COLORS['yellow']}history{COLORS['reset']} : Show conversation history")
|
56
|
+
print(f" {COLORS['yellow']}clear{COLORS['reset']} : Reset conversation")
|
57
|
+
print(f" {COLORS['yellow']}exit{COLORS['reset']} : End session")
|
58
|
+
|
59
|
+
print(f"\n{separator}\n")
|
60
|
+
|
61
|
+
# Initialize log file if provided
|
62
|
+
log_handle = None
|
63
|
+
if log_file:
|
64
|
+
try:
|
65
|
+
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
66
|
+
log_handle = open(log_file, 'a', encoding='utf-8')
|
67
|
+
log_handle.write(f"\n--- nGPT Session Log: {sys.argv} ---\n")
|
68
|
+
log_handle.write(f"Started at: {timestamp}\n\n")
|
69
|
+
print(f"{COLORS['green']}Logging conversation to: {log_file}{COLORS['reset']}")
|
70
|
+
except Exception as e:
|
71
|
+
print(f"{COLORS['yellow']}Warning: Could not open log file: {str(e)}{COLORS['reset']}")
|
72
|
+
log_handle = None
|
73
|
+
|
74
|
+
# Custom separator - use the same length for consistency
|
75
|
+
def print_separator():
|
76
|
+
print(f"\n{separator}\n")
|
77
|
+
|
78
|
+
# Initialize conversation history
|
79
|
+
system_prompt = preprompt if preprompt else "You are a helpful assistant."
|
80
|
+
|
81
|
+
# Add markdown formatting instruction to system prompt if prettify is enabled
|
82
|
+
if prettify:
|
83
|
+
if system_prompt:
|
84
|
+
system_prompt += " You can use markdown formatting in your responses where appropriate."
|
85
|
+
else:
|
86
|
+
system_prompt = "You are a helpful assistant. You can use markdown formatting in your responses where appropriate."
|
87
|
+
|
88
|
+
conversation = []
|
89
|
+
system_message = {"role": "system", "content": system_prompt}
|
90
|
+
conversation.append(system_message)
|
91
|
+
|
92
|
+
# Log system prompt if logging is enabled
|
93
|
+
if log_handle and preprompt:
|
94
|
+
log_handle.write(f"System: {system_prompt}\n\n")
|
95
|
+
log_handle.flush()
|
96
|
+
|
97
|
+
# Initialize prompt_toolkit history
|
98
|
+
prompt_history = InMemoryHistory() if HAS_PROMPT_TOOLKIT else None
|
99
|
+
|
100
|
+
# Decorative chat headers with rounded corners
|
101
|
+
def user_header():
|
102
|
+
return f"{COLORS['cyan']}{COLORS['bold']}╭─ 👤 You {COLORS['reset']}"
|
103
|
+
|
104
|
+
def ngpt_header():
|
105
|
+
return f"{COLORS['green']}{COLORS['bold']}╭─ 🤖 nGPT {COLORS['reset']}"
|
106
|
+
|
107
|
+
# Function to display conversation history
|
108
|
+
def display_history():
|
109
|
+
if len(conversation) <= 1: # Only system message
|
110
|
+
print(f"\n{COLORS['yellow']}No conversation history yet.{COLORS['reset']}")
|
111
|
+
return
|
112
|
+
|
113
|
+
print(f"\n{COLORS['cyan']}{COLORS['bold']}Conversation History:{COLORS['reset']}")
|
114
|
+
print(separator)
|
115
|
+
|
116
|
+
# Skip system message
|
117
|
+
message_count = 0
|
118
|
+
for i, msg in enumerate(conversation):
|
119
|
+
if msg["role"] == "system":
|
120
|
+
continue
|
121
|
+
|
122
|
+
if msg["role"] == "user":
|
123
|
+
message_count += 1
|
124
|
+
print(f"\n{user_header()}")
|
125
|
+
print(f"{COLORS['cyan']}│ [{message_count}] {COLORS['reset']}{msg['content']}")
|
126
|
+
elif msg["role"] == "assistant":
|
127
|
+
print(f"\n{ngpt_header()}")
|
128
|
+
print(f"{COLORS['green']}│ {COLORS['reset']}{msg['content']}")
|
129
|
+
|
130
|
+
print(f"\n{separator}") # Consistent separator at the end
|
131
|
+
|
132
|
+
# Function to clear conversation history
|
133
|
+
def clear_history():
|
134
|
+
nonlocal conversation
|
135
|
+
conversation = [{"role": "system", "content": system_prompt}]
|
136
|
+
print(f"\n{COLORS['yellow']}Conversation history cleared.{COLORS['reset']}")
|
137
|
+
print(separator) # Add separator for consistency
|
138
|
+
|
139
|
+
try:
|
140
|
+
while True:
|
141
|
+
# Get user input
|
142
|
+
if HAS_PROMPT_TOOLKIT:
|
143
|
+
# Custom styling for prompt_toolkit
|
144
|
+
style = Style.from_dict({
|
145
|
+
'prompt': 'ansicyan bold',
|
146
|
+
'input': 'ansiwhite',
|
147
|
+
})
|
148
|
+
|
149
|
+
# Create key bindings for Ctrl+C handling
|
150
|
+
kb = KeyBindings()
|
151
|
+
@kb.add('c-c')
|
152
|
+
def _(event):
|
153
|
+
event.app.exit(result=None)
|
154
|
+
raise KeyboardInterrupt()
|
155
|
+
|
156
|
+
# Get user input with styled prompt - using proper HTML formatting
|
157
|
+
user_input = pt_prompt(
|
158
|
+
HTML("<ansicyan><b>╭─ 👤 You:</b></ansicyan> "),
|
159
|
+
style=style,
|
160
|
+
key_bindings=kb,
|
161
|
+
history=prompt_history
|
162
|
+
)
|
163
|
+
else:
|
164
|
+
user_input = input(f"{user_header()}: {COLORS['reset']}")
|
165
|
+
|
166
|
+
# Check for exit commands
|
167
|
+
if user_input.lower() in ('exit', 'quit', 'bye'):
|
168
|
+
print(f"\n{COLORS['green']}Ending chat session. Goodbye!{COLORS['reset']}")
|
169
|
+
break
|
170
|
+
|
171
|
+
# Check for special commands
|
172
|
+
if user_input.lower() == 'history':
|
173
|
+
display_history()
|
174
|
+
continue
|
175
|
+
|
176
|
+
if user_input.lower() == 'clear':
|
177
|
+
clear_history()
|
178
|
+
continue
|
179
|
+
|
180
|
+
# Skip empty messages but don't raise an error
|
181
|
+
if not user_input.strip():
|
182
|
+
print(f"{COLORS['yellow']}Empty message skipped. Type 'exit' to quit.{COLORS['reset']}")
|
183
|
+
continue
|
184
|
+
|
185
|
+
# Add user message to conversation
|
186
|
+
user_message = {"role": "user", "content": user_input}
|
187
|
+
conversation.append(user_message)
|
188
|
+
|
189
|
+
# Log user message if logging is enabled
|
190
|
+
if log_handle:
|
191
|
+
log_handle.write(f"User: {user_input}\n")
|
192
|
+
log_handle.flush()
|
193
|
+
|
194
|
+
# Print assistant indicator with formatting
|
195
|
+
if not no_stream and not stream_prettify:
|
196
|
+
print(f"\n{ngpt_header()}: {COLORS['reset']}", end="", flush=True)
|
197
|
+
elif not stream_prettify:
|
198
|
+
print(f"\n{ngpt_header()}: {COLORS['reset']}", flush=True)
|
199
|
+
|
200
|
+
# If prettify is enabled with regular streaming
|
201
|
+
if prettify and not no_stream and not stream_prettify:
|
202
|
+
print(f"\n{COLORS['yellow']}Note: Streaming disabled to enable markdown rendering.{COLORS['reset']}")
|
203
|
+
print(f"\n{ngpt_header()}: {COLORS['reset']}", flush=True)
|
204
|
+
should_stream = False
|
205
|
+
else:
|
206
|
+
# Regular behavior with stream-prettify taking precedence
|
207
|
+
should_stream = not no_stream
|
208
|
+
|
209
|
+
# Setup for stream-prettify
|
210
|
+
stream_callback = None
|
211
|
+
live_display = None
|
212
|
+
|
213
|
+
if stream_prettify and should_stream:
|
214
|
+
# Get the correct header for interactive mode
|
215
|
+
header = ngpt_header()
|
216
|
+
live_display, stream_callback = prettify_streaming_markdown(renderer, is_interactive=True, header_text=header)
|
217
|
+
if not live_display:
|
218
|
+
# Fallback to normal prettify if live display setup failed
|
219
|
+
prettify = True
|
220
|
+
stream_prettify = False
|
221
|
+
should_stream = False
|
222
|
+
print(f"{COLORS['yellow']}Falling back to regular prettify mode.{COLORS['reset']}")
|
223
|
+
|
224
|
+
# Start live display if using stream-prettify
|
225
|
+
if stream_prettify and live_display:
|
226
|
+
live_display.start()
|
227
|
+
|
228
|
+
# Get AI response with conversation history
|
229
|
+
response = client.chat(
|
230
|
+
prompt=user_input,
|
231
|
+
messages=conversation,
|
232
|
+
stream=should_stream,
|
233
|
+
web_search=web_search,
|
234
|
+
temperature=temperature,
|
235
|
+
top_p=top_p,
|
236
|
+
max_tokens=max_tokens,
|
237
|
+
markdown_format=prettify or stream_prettify,
|
238
|
+
stream_callback=stream_callback
|
239
|
+
)
|
240
|
+
|
241
|
+
# Stop live display if using stream-prettify
|
242
|
+
if stream_prettify and live_display:
|
243
|
+
live_display.stop()
|
244
|
+
|
245
|
+
# Add AI response to conversation history
|
246
|
+
if response:
|
247
|
+
assistant_message = {"role": "assistant", "content": response}
|
248
|
+
conversation.append(assistant_message)
|
249
|
+
|
250
|
+
# Print response if not streamed (either due to no_stream or prettify)
|
251
|
+
if no_stream or prettify:
|
252
|
+
if prettify:
|
253
|
+
prettify_markdown(response, renderer)
|
254
|
+
else:
|
255
|
+
print(response)
|
256
|
+
|
257
|
+
# Log assistant response if logging is enabled
|
258
|
+
if log_handle:
|
259
|
+
log_handle.write(f"Assistant: {response}\n\n")
|
260
|
+
log_handle.flush()
|
261
|
+
|
262
|
+
# Print separator between exchanges
|
263
|
+
print_separator()
|
264
|
+
|
265
|
+
except KeyboardInterrupt:
|
266
|
+
print(f"\n\n{COLORS['green']}Chat session ended by user. Goodbye!{COLORS['reset']}")
|
267
|
+
except Exception as e:
|
268
|
+
print(f"\n{COLORS['yellow']}Error during chat session: {str(e)}{COLORS['reset']}")
|
269
|
+
# Print traceback for debugging if it's a serious error
|
270
|
+
traceback.print_exc()
|
271
|
+
finally:
|
272
|
+
# Close log file if it was opened
|
273
|
+
if log_handle:
|
274
|
+
log_handle.write(f"\n--- End of Session ---\n")
|
275
|
+
log_handle.close()
|