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 ADDED
@@ -0,0 +1,3 @@
1
+ from .main import main
2
+
3
+ __all__ = ['main']
@@ -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
@@ -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()