ngpt 3.8.2__py3-none-any.whl → 3.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ngpt/cli/args.py +43 -2
- ngpt/cli/main.py +17 -1
- ngpt/cli/modes/chat.py +18 -13
- ngpt/cli/modes/code.py +20 -15
- ngpt/cli/modes/interactive.py +53 -46
- ngpt/cli/modes/rewrite.py +18 -13
- ngpt/cli/modes/shell.py +15 -13
- ngpt/cli/modes/text.py +18 -13
- ngpt/cli/renderers.py +56 -50
- ngpt/cli/roles.py +265 -0
- ngpt/cli/ui.py +17 -4
- {ngpt-3.8.2.dist-info → ngpt-3.9.0.dist-info}/METADATA +50 -6
- ngpt-3.9.0.dist-info/RECORD +30 -0
- ngpt-3.8.2.dist-info/RECORD +0 -29
- {ngpt-3.8.2.dist-info → ngpt-3.9.0.dist-info}/WHEEL +0 -0
- {ngpt-3.8.2.dist-info → ngpt-3.9.0.dist-info}/entry_points.txt +0 -0
- {ngpt-3.8.2.dist-info → ngpt-3.9.0.dist-info}/licenses/LICENSE +0 -0
ngpt/cli/args.py
CHANGED
@@ -58,6 +58,10 @@ def setup_argument_parser():
|
|
58
58
|
help='Show available markdown renderers for use with --prettify')
|
59
59
|
config_group.add_argument('--cli-config', nargs='*', metavar='COMMAND',
|
60
60
|
help='Manage CLI configuration (set, get, unset, list, help)')
|
61
|
+
|
62
|
+
# Role configuration options
|
63
|
+
config_group.add_argument('--role-config', nargs='*', metavar='ACTION',
|
64
|
+
help='Manage custom roles (help, create, show, edit, list, remove) [role_name]')
|
61
65
|
|
62
66
|
# Global options
|
63
67
|
global_group = parser.add_argument_group('Global Options')
|
@@ -79,8 +83,13 @@ def setup_argument_parser():
|
|
79
83
|
help='Set max response length in tokens')
|
80
84
|
global_group.add_argument('--log', metavar='FILE', nargs='?', const=True,
|
81
85
|
help='Set filepath to log conversation to, or create a temporary log file if no path provided')
|
82
|
-
|
86
|
+
|
87
|
+
# System prompt options (mutually exclusive)
|
88
|
+
prompt_exclusive_group = global_group.add_mutually_exclusive_group()
|
89
|
+
prompt_exclusive_group.add_argument('--preprompt',
|
83
90
|
help='Set custom system prompt to control AI behavior')
|
91
|
+
prompt_exclusive_group.add_argument('--role',
|
92
|
+
help='Use a predefined role to set system prompt (mutually exclusive with --preprompt)')
|
84
93
|
|
85
94
|
# Output display options (mutually exclusive group)
|
86
95
|
output_group = parser.add_argument_group('Output Display Options (mutually exclusive)')
|
@@ -226,4 +235,36 @@ def handle_cli_config_args(args):
|
|
226
235
|
return (True, action, option, value)
|
227
236
|
else:
|
228
237
|
# Unknown action, show help
|
229
|
-
return (True, "help", None, None)
|
238
|
+
return (True, "help", None, None)
|
239
|
+
|
240
|
+
def handle_role_config_args(args):
|
241
|
+
"""Process role configuration arguments and determine command parameters.
|
242
|
+
|
243
|
+
Args:
|
244
|
+
args: The parsed command line arguments.
|
245
|
+
|
246
|
+
Returns:
|
247
|
+
tuple: (should_handle, action, role_name)
|
248
|
+
- should_handle: True if --role-config was specified and should be handled
|
249
|
+
- action: The action to perform (help, create, show, edit, list, remove)
|
250
|
+
- role_name: The name of the role (or None for actions like list and help)
|
251
|
+
"""
|
252
|
+
if args.role_config is None:
|
253
|
+
return (False, None, None)
|
254
|
+
|
255
|
+
# Show help if no arguments or "help" argument
|
256
|
+
if len(args.role_config) == 0 or (len(args.role_config) > 0 and args.role_config[0].lower() == "help"):
|
257
|
+
return (True, "help", None)
|
258
|
+
|
259
|
+
action = args.role_config[0].lower()
|
260
|
+
role_name = args.role_config[1] if len(args.role_config) > 1 else None
|
261
|
+
|
262
|
+
# If action requires a role name but none is provided
|
263
|
+
if action in ("create", "show", "edit", "remove") and role_name is None:
|
264
|
+
raise ValueError(f"--role-config {action} requires a role name")
|
265
|
+
|
266
|
+
if action in ("help", "create", "show", "edit", "list", "remove"):
|
267
|
+
return (True, action, role_name)
|
268
|
+
else:
|
269
|
+
# Unknown action, show help
|
270
|
+
return (True, "help", None)
|
ngpt/cli/main.py
CHANGED
@@ -25,7 +25,8 @@ from .modes.shell import shell_mode
|
|
25
25
|
from .modes.text import text_mode
|
26
26
|
from .modes.rewrite import rewrite_mode
|
27
27
|
from .modes.gitcommsg import gitcommsg_mode
|
28
|
-
from .args import parse_args, validate_args, handle_cli_config_args, setup_argument_parser, validate_markdown_renderer
|
28
|
+
from .args import parse_args, validate_args, handle_cli_config_args, setup_argument_parser, validate_markdown_renderer, handle_role_config_args
|
29
|
+
from .roles import handle_role_config, get_role_prompt
|
29
30
|
|
30
31
|
def show_cli_config_help():
|
31
32
|
"""Display help information about CLI configuration."""
|
@@ -194,6 +195,12 @@ def main():
|
|
194
195
|
handle_cli_config(action, option, value)
|
195
196
|
return
|
196
197
|
|
198
|
+
# Handle role configuration command
|
199
|
+
should_handle_role_config, action, role_name = handle_role_config_args(args)
|
200
|
+
if should_handle_role_config:
|
201
|
+
handle_role_config(action, role_name)
|
202
|
+
return
|
203
|
+
|
197
204
|
# Handle --renderers flag to show available markdown renderers
|
198
205
|
if args.list_renderers:
|
199
206
|
show_available_renderers()
|
@@ -484,6 +491,15 @@ def main():
|
|
484
491
|
if not has_renderer:
|
485
492
|
show_available_renderers()
|
486
493
|
|
494
|
+
# Get system prompt from role if specified
|
495
|
+
if args.role:
|
496
|
+
role_prompt = get_role_prompt(args.role)
|
497
|
+
if role_prompt:
|
498
|
+
args.preprompt = role_prompt
|
499
|
+
else:
|
500
|
+
# If role doesn't exist, exit
|
501
|
+
return
|
502
|
+
|
487
503
|
# Initialize client using the potentially overridden active_config
|
488
504
|
client = NGPTClient(
|
489
505
|
api_key=active_config.get("api_key", args.api_key),
|
ngpt/cli/modes/chat.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from ..formatters import COLORS
|
2
|
-
from ..renderers import prettify_markdown, prettify_streaming_markdown
|
2
|
+
from ..renderers import prettify_markdown, prettify_streaming_markdown, TERMINAL_RENDER_LOCK
|
3
3
|
from ..ui import spinner
|
4
4
|
from ...utils import enhance_prompt_with_web_search, process_piped_input
|
5
5
|
import sys
|
@@ -53,9 +53,10 @@ def chat_mode(client, args, logger=None):
|
|
53
53
|
stop_spinner.set()
|
54
54
|
spinner_thread.join()
|
55
55
|
# Clear the spinner line completely
|
56
|
-
|
57
|
-
|
58
|
-
|
56
|
+
with TERMINAL_RENDER_LOCK:
|
57
|
+
sys.stdout.write("\r" + " " * 100 + "\r")
|
58
|
+
sys.stdout.flush()
|
59
|
+
print("Enhanced input with web search results.")
|
59
60
|
except Exception as e:
|
60
61
|
# Stop the spinner before re-raising
|
61
62
|
stop_spinner.set()
|
@@ -129,11 +130,14 @@ def chat_mode(client, args, logger=None):
|
|
129
130
|
# On first content, stop the spinner
|
130
131
|
if not first_content_received and stop_spinner_func:
|
131
132
|
first_content_received = True
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
133
|
+
|
134
|
+
# Use lock to prevent terminal rendering conflicts
|
135
|
+
with TERMINAL_RENDER_LOCK:
|
136
|
+
# Stop the spinner
|
137
|
+
stop_spinner_func()
|
138
|
+
# Ensure spinner message is cleared with an extra blank line
|
139
|
+
sys.stdout.write("\r" + " " * 100 + "\r")
|
140
|
+
sys.stdout.flush()
|
137
141
|
|
138
142
|
# Call the original callback to update the display
|
139
143
|
if original_callback:
|
@@ -165,7 +169,8 @@ def chat_mode(client, args, logger=None):
|
|
165
169
|
|
166
170
|
# Handle non-stream response or regular prettify
|
167
171
|
if (args.no_stream or args.prettify) and response:
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
+
with TERMINAL_RENDER_LOCK:
|
173
|
+
if args.prettify:
|
174
|
+
prettify_markdown(response, args.renderer)
|
175
|
+
else:
|
176
|
+
print(response)
|
ngpt/cli/modes/code.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from ..formatters import COLORS
|
2
|
-
from ..renderers import prettify_markdown, prettify_streaming_markdown, has_markdown_renderer, show_available_renderers
|
2
|
+
from ..renderers import prettify_markdown, prettify_streaming_markdown, has_markdown_renderer, show_available_renderers, TERMINAL_RENDER_LOCK
|
3
3
|
from ..ui import spinner, copy_to_clipboard
|
4
4
|
from ...utils import enhance_prompt_with_web_search, process_piped_input
|
5
5
|
import sys
|
@@ -126,9 +126,10 @@ def code_mode(client, args, logger=None):
|
|
126
126
|
stop_spinner.set()
|
127
127
|
spinner_thread.join()
|
128
128
|
# Clear the spinner line completely
|
129
|
-
|
130
|
-
|
131
|
-
|
129
|
+
with TERMINAL_RENDER_LOCK:
|
130
|
+
sys.stdout.write("\r" + " " * 100 + "\r")
|
131
|
+
sys.stdout.flush()
|
132
|
+
print("Enhanced input with web search results.")
|
132
133
|
except Exception as e:
|
133
134
|
# Stop the spinner before re-raising
|
134
135
|
stop_spinner.set()
|
@@ -211,11 +212,14 @@ def code_mode(client, args, logger=None):
|
|
211
212
|
# On first content, stop the spinner
|
212
213
|
if not first_content_received and stop_spinner_func:
|
213
214
|
first_content_received = True
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
215
|
+
|
216
|
+
# Use lock to prevent terminal rendering conflicts
|
217
|
+
with TERMINAL_RENDER_LOCK:
|
218
|
+
# Stop the spinner
|
219
|
+
stop_spinner_func()
|
220
|
+
# Ensure spinner message is cleared with an extra blank line
|
221
|
+
sys.stdout.write("\r" + " " * 100 + "\r")
|
222
|
+
sys.stdout.flush()
|
219
223
|
|
220
224
|
# Call the original callback to update the display
|
221
225
|
if original_callback:
|
@@ -297,12 +301,13 @@ def code_mode(client, args, logger=None):
|
|
297
301
|
|
298
302
|
# Print non-streamed output if needed
|
299
303
|
if generated_code and not should_stream:
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
304
|
+
with TERMINAL_RENDER_LOCK:
|
305
|
+
if use_regular_prettify:
|
306
|
+
print("\nGenerated code:")
|
307
|
+
prettify_markdown(generated_code, args.renderer)
|
308
|
+
else:
|
309
|
+
# Should only happen if --no-stream was used without prettify
|
310
|
+
print(f"\nGenerated code:\n{generated_code}")
|
306
311
|
|
307
312
|
# Offer to copy to clipboard
|
308
313
|
if generated_code and not args.no_stream:
|
ngpt/cli/modes/interactive.py
CHANGED
@@ -5,7 +5,7 @@ import threading
|
|
5
5
|
import sys
|
6
6
|
import time
|
7
7
|
from ..formatters import COLORS
|
8
|
-
from ..renderers import prettify_markdown, prettify_streaming_markdown
|
8
|
+
from ..renderers import prettify_markdown, prettify_streaming_markdown, TERMINAL_RENDER_LOCK
|
9
9
|
from ..ui import spinner
|
10
10
|
from ...utils import enhance_prompt_with_web_search
|
11
11
|
|
@@ -78,8 +78,9 @@ def interactive_chat_session(client, web_search=False, no_stream=False, temperat
|
|
78
78
|
def print_separator():
|
79
79
|
# Make sure there's exactly one newline before and after
|
80
80
|
# Use sys.stdout.write for direct control, avoiding any extra newlines
|
81
|
-
|
82
|
-
|
81
|
+
with TERMINAL_RENDER_LOCK:
|
82
|
+
sys.stdout.write(f"\n{separator}\n")
|
83
|
+
sys.stdout.flush()
|
83
84
|
|
84
85
|
# Initialize conversation history
|
85
86
|
system_prompt = preprompt if preprompt else "You are a helpful assistant."
|
@@ -111,35 +112,37 @@ def interactive_chat_session(client, web_search=False, no_stream=False, temperat
|
|
111
112
|
|
112
113
|
# Function to display conversation history
|
113
114
|
def display_history():
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
print(f"\n{COLORS['cyan']}{COLORS['bold']}Conversation History:{COLORS['reset']}")
|
119
|
-
print(separator)
|
120
|
-
|
121
|
-
# Skip system message
|
122
|
-
message_count = 0
|
123
|
-
for i, msg in enumerate(conversation):
|
124
|
-
if msg["role"] == "system":
|
125
|
-
continue
|
115
|
+
with TERMINAL_RENDER_LOCK:
|
116
|
+
if len(conversation) <= 1: # Only system message
|
117
|
+
print(f"\n{COLORS['yellow']}No conversation history yet.{COLORS['reset']}")
|
118
|
+
return
|
126
119
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
120
|
+
print(f"\n{COLORS['cyan']}{COLORS['bold']}Conversation History:{COLORS['reset']}")
|
121
|
+
print(separator)
|
122
|
+
|
123
|
+
# Skip system message
|
124
|
+
message_count = 0
|
125
|
+
for i, msg in enumerate(conversation):
|
126
|
+
if msg["role"] == "system":
|
127
|
+
continue
|
128
|
+
|
129
|
+
if msg["role"] == "user":
|
130
|
+
message_count += 1
|
131
|
+
print(f"\n{user_header()}")
|
132
|
+
print(f"{COLORS['cyan']}│ [{message_count}] {COLORS['reset']}{msg['content']}")
|
133
|
+
elif msg["role"] == "assistant":
|
134
|
+
print(f"\n{ngpt_header()}")
|
135
|
+
print(f"{COLORS['green']}│ {COLORS['reset']}{msg['content']}")
|
136
|
+
|
137
|
+
print(f"\n{separator}") # Consistent separator at the end
|
136
138
|
|
137
139
|
# Function to clear conversation history
|
138
140
|
def clear_history():
|
139
141
|
nonlocal conversation
|
140
142
|
conversation = [{"role": "system", "content": system_prompt}]
|
141
|
-
|
142
|
-
|
143
|
+
with TERMINAL_RENDER_LOCK:
|
144
|
+
print(f"\n{COLORS['yellow']}Conversation history cleared.{COLORS['reset']}")
|
145
|
+
print(separator) # Add separator for consistency
|
143
146
|
|
144
147
|
try:
|
145
148
|
while True:
|
@@ -249,10 +252,11 @@ def interactive_chat_session(client, web_search=False, no_stream=False, temperat
|
|
249
252
|
|
250
253
|
# Print the header if needed
|
251
254
|
if should_print_header:
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
255
|
+
with TERMINAL_RENDER_LOCK:
|
256
|
+
if not no_stream and not stream_prettify:
|
257
|
+
print(f"\n{ngpt_header()}: {COLORS['reset']}", end="", flush=True)
|
258
|
+
elif not stream_prettify:
|
259
|
+
print(f"\n{ngpt_header()}: {COLORS['reset']}", flush=True)
|
256
260
|
|
257
261
|
# Determine streaming behavior
|
258
262
|
if prettify and not no_stream and not stream_prettify:
|
@@ -294,18 +298,20 @@ def interactive_chat_session(client, web_search=False, no_stream=False, temperat
|
|
294
298
|
if not first_content_received:
|
295
299
|
first_content_received = True
|
296
300
|
|
297
|
-
#
|
298
|
-
|
299
|
-
|
301
|
+
# Use lock to prevent terminal rendering conflicts
|
302
|
+
with TERMINAL_RENDER_LOCK:
|
303
|
+
# Stop the spinner if it's running
|
304
|
+
if stop_spinner_func:
|
305
|
+
stop_spinner_func()
|
306
|
+
|
307
|
+
# Clear the spinner line completely without leaving extra newlines
|
308
|
+
# Use direct terminal control to ensure consistency
|
309
|
+
sys.stdout.write("\r" + " " * shutil.get_terminal_size().columns + "\r")
|
310
|
+
sys.stdout.flush()
|
300
311
|
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
sys.stdout.flush()
|
305
|
-
|
306
|
-
# Now start the live display
|
307
|
-
if live_display:
|
308
|
-
live_display.start()
|
312
|
+
# Now start the live display
|
313
|
+
if live_display:
|
314
|
+
live_display.start()
|
309
315
|
|
310
316
|
# Call the original callback to update content
|
311
317
|
if original_callback:
|
@@ -351,11 +357,12 @@ def interactive_chat_session(client, web_search=False, no_stream=False, temperat
|
|
351
357
|
|
352
358
|
# Print response if not streamed (either due to no_stream or prettify)
|
353
359
|
if no_stream or prettify:
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
360
|
+
with TERMINAL_RENDER_LOCK:
|
361
|
+
if prettify:
|
362
|
+
# For pretty formatting with rich, don't print any header text as the rich renderer already includes it
|
363
|
+
prettify_markdown(response, renderer)
|
364
|
+
else:
|
365
|
+
print(response)
|
359
366
|
|
360
367
|
# Log AI response if logging is enabled
|
361
368
|
if logger:
|
ngpt/cli/modes/rewrite.py
CHANGED
@@ -2,7 +2,7 @@ import sys
|
|
2
2
|
import threading
|
3
3
|
import time
|
4
4
|
from ..formatters import COLORS
|
5
|
-
from ..renderers import prettify_markdown, prettify_streaming_markdown
|
5
|
+
from ..renderers import prettify_markdown, prettify_streaming_markdown, TERMINAL_RENDER_LOCK
|
6
6
|
from ..ui import get_multiline_input, spinner, copy_to_clipboard
|
7
7
|
from ...utils import enhance_prompt_with_web_search, process_piped_input
|
8
8
|
|
@@ -127,9 +127,10 @@ def rewrite_mode(client, args, logger=None):
|
|
127
127
|
stop_spinner.set()
|
128
128
|
spinner_thread.join()
|
129
129
|
# Clear the spinner line completely
|
130
|
-
|
131
|
-
|
132
|
-
|
130
|
+
with TERMINAL_RENDER_LOCK:
|
131
|
+
sys.stdout.write("\r" + " " * 100 + "\r")
|
132
|
+
sys.stdout.flush()
|
133
|
+
print("Enhanced input with web search results.")
|
133
134
|
except Exception as e:
|
134
135
|
# Stop the spinner before re-raising
|
135
136
|
stop_spinner.set()
|
@@ -197,11 +198,14 @@ def rewrite_mode(client, args, logger=None):
|
|
197
198
|
# On first content, stop the spinner
|
198
199
|
if not first_content_received and stop_spinner_func:
|
199
200
|
first_content_received = True
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
201
|
+
|
202
|
+
# Use lock to prevent terminal rendering conflicts
|
203
|
+
with TERMINAL_RENDER_LOCK:
|
204
|
+
# Stop the spinner
|
205
|
+
stop_spinner_func()
|
206
|
+
# Ensure spinner message is cleared with an extra blank line
|
207
|
+
sys.stdout.write("\r" + " " * 100 + "\r\n")
|
208
|
+
sys.stdout.flush()
|
205
209
|
|
206
210
|
# Call the original callback to update the display
|
207
211
|
if original_callback:
|
@@ -241,10 +245,11 @@ def rewrite_mode(client, args, logger=None):
|
|
241
245
|
|
242
246
|
# Handle non-stream response or regular prettify
|
243
247
|
if (args.no_stream or args.prettify) and response:
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
+
with TERMINAL_RENDER_LOCK:
|
249
|
+
if args.prettify:
|
250
|
+
prettify_markdown(response, args.renderer)
|
251
|
+
else:
|
252
|
+
print(response)
|
248
253
|
|
249
254
|
# Offer to copy to clipboard if not in a redirected output
|
250
255
|
if not args.no_stream and response:
|
ngpt/cli/modes/shell.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from ..formatters import COLORS
|
2
2
|
from ..ui import spinner, copy_to_clipboard, get_terminal_input
|
3
|
-
from ..renderers import prettify_markdown, has_markdown_renderer, prettify_streaming_markdown, show_available_renderers
|
3
|
+
from ..renderers import prettify_markdown, has_markdown_renderer, prettify_streaming_markdown, show_available_renderers, TERMINAL_RENDER_LOCK
|
4
4
|
from ...utils import enhance_prompt_with_web_search, process_piped_input
|
5
5
|
import subprocess
|
6
6
|
import sys
|
@@ -796,19 +796,21 @@ def shell_mode(client, args, logger=None):
|
|
796
796
|
]
|
797
797
|
prompt_text = f"\nWhat would you like to do? [{COLORS['cyan']}C{COLORS['reset']}/{COLORS['cyan']}E{COLORS['reset']}/{COLORS['cyan']}D{COLORS['reset']}/{COLORS['cyan']}A{COLORS['reset']}] "
|
798
798
|
|
799
|
-
#
|
800
|
-
|
801
|
-
|
802
|
-
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
807
|
-
|
799
|
+
# Make sure box rendering is complete before showing options
|
800
|
+
with TERMINAL_RENDER_LOCK:
|
801
|
+
# Add a small delay to ensure terminal rendering is complete,
|
802
|
+
# especially important for stream-prettify mode
|
803
|
+
if use_stream_prettify:
|
804
|
+
time.sleep(0.5)
|
805
|
+
|
806
|
+
# Print options with proper flushing to ensure display
|
807
|
+
print(options_text, flush=True)
|
808
|
+
for option in options:
|
809
|
+
print(option, flush=True)
|
808
810
|
|
809
|
-
|
810
|
-
|
811
|
-
|
811
|
+
# Print prompt and flush to ensure it appears
|
812
|
+
sys.stdout.write(prompt_text)
|
813
|
+
sys.stdout.flush()
|
812
814
|
|
813
815
|
try:
|
814
816
|
# Use get_terminal_input which opens /dev/tty directly rather than using stdin
|
ngpt/cli/modes/text.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from ..formatters import COLORS
|
2
|
-
from ..renderers import prettify_markdown, prettify_streaming_markdown
|
2
|
+
from ..renderers import prettify_markdown, prettify_streaming_markdown, TERMINAL_RENDER_LOCK
|
3
3
|
from ..ui import get_multiline_input, spinner
|
4
4
|
from ...utils import enhance_prompt_with_web_search
|
5
5
|
import threading
|
@@ -47,9 +47,10 @@ def text_mode(client, args, logger=None):
|
|
47
47
|
stop_spinner.set()
|
48
48
|
spinner_thread.join()
|
49
49
|
# Clear the spinner line completely
|
50
|
-
|
51
|
-
|
52
|
-
|
50
|
+
with TERMINAL_RENDER_LOCK:
|
51
|
+
sys.stdout.write("\r" + " " * 100 + "\r")
|
52
|
+
sys.stdout.flush()
|
53
|
+
print("Enhanced input with web search results.")
|
53
54
|
except Exception as e:
|
54
55
|
# Stop the spinner before re-raising
|
55
56
|
stop_spinner.set()
|
@@ -119,11 +120,14 @@ def text_mode(client, args, logger=None):
|
|
119
120
|
# On first content, stop the spinner
|
120
121
|
if not first_content_received and stop_spinner_func:
|
121
122
|
first_content_received = True
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
123
|
+
|
124
|
+
# Use lock to prevent terminal rendering conflicts
|
125
|
+
with TERMINAL_RENDER_LOCK:
|
126
|
+
# Stop the spinner
|
127
|
+
stop_spinner_func()
|
128
|
+
# Ensure spinner message is cleared with an extra blank line
|
129
|
+
sys.stdout.write("\r" + " " * 100 + "\r")
|
130
|
+
sys.stdout.flush()
|
127
131
|
|
128
132
|
# Call the original callback to update the display
|
129
133
|
if original_callback:
|
@@ -155,7 +159,8 @@ def text_mode(client, args, logger=None):
|
|
155
159
|
|
156
160
|
# Handle non-stream response or regular prettify
|
157
161
|
if (args.no_stream or args.prettify) and response:
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
+
with TERMINAL_RENDER_LOCK:
|
163
|
+
if args.prettify:
|
164
|
+
prettify_markdown(response, args.renderer)
|
165
|
+
else:
|
166
|
+
print(response)
|
ngpt/cli/renderers.py
CHANGED
@@ -3,8 +3,12 @@ import shutil
|
|
3
3
|
import subprocess
|
4
4
|
import tempfile
|
5
5
|
import sys
|
6
|
+
import threading
|
6
7
|
from .formatters import COLORS
|
7
8
|
|
9
|
+
# Global lock for terminal rendering to prevent race conditions
|
10
|
+
TERMINAL_RENDER_LOCK = threading.Lock()
|
11
|
+
|
8
12
|
# Try to import markdown rendering libraries
|
9
13
|
try:
|
10
14
|
import rich
|
@@ -308,63 +312,65 @@ def prettify_streaming_markdown(renderer='rich', is_interactive=False, header_te
|
|
308
312
|
# Check if this is the final update (complete flag)
|
309
313
|
is_complete = kwargs.get('complete', False)
|
310
314
|
|
311
|
-
#
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
# Update
|
321
|
-
if
|
322
|
-
#
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
315
|
+
# Use lock to prevent terminal rendering conflicts
|
316
|
+
with TERMINAL_RENDER_LOCK:
|
317
|
+
# Start live display on first content
|
318
|
+
if first_update:
|
319
|
+
first_update = False
|
320
|
+
# Let the spinner's clean_exit handle the cleanup
|
321
|
+
# No additional cleanup needed here
|
322
|
+
live.start()
|
323
|
+
|
324
|
+
# Update content in live display
|
325
|
+
if is_interactive and header_text:
|
326
|
+
# Update the panel content - for streaming, only show the last portion that fits in display_height
|
327
|
+
if not is_complete:
|
328
|
+
# Calculate approximate lines needed (rough estimation)
|
329
|
+
content_lines = content.count('\n') + 1
|
330
|
+
available_height = display_height - 4 # Account for panel borders and padding
|
331
|
+
|
332
|
+
if content_lines > available_height:
|
333
|
+
# If content is too big, show only the last part that fits
|
334
|
+
lines = content.split('\n')
|
335
|
+
truncated_content = '\n'.join(lines[-available_height:])
|
336
|
+
md_obj.renderable = Markdown(truncated_content)
|
337
|
+
else:
|
338
|
+
md_obj.renderable = Markdown(content)
|
331
339
|
else:
|
332
340
|
md_obj.renderable = Markdown(content)
|
333
|
-
else:
|
334
|
-
md_obj.renderable = Markdown(content)
|
335
|
-
|
336
|
-
live.update(md_obj)
|
337
|
-
else:
|
338
|
-
# Same logic for non-interactive mode
|
339
|
-
if not is_complete:
|
340
|
-
# Calculate approximate lines needed
|
341
|
-
content_lines = content.count('\n') + 1
|
342
|
-
available_height = display_height - 4 # Account for panel borders and padding
|
343
341
|
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
342
|
+
live.update(md_obj)
|
343
|
+
else:
|
344
|
+
# Same logic for non-interactive mode
|
345
|
+
if not is_complete:
|
346
|
+
# Calculate approximate lines needed
|
347
|
+
content_lines = content.count('\n') + 1
|
348
|
+
available_height = display_height - 4 # Account for panel borders and padding
|
349
|
+
|
350
|
+
if content_lines > available_height:
|
351
|
+
# If content is too big, show only the last part that fits
|
352
|
+
lines = content.split('\n')
|
353
|
+
truncated_content = '\n'.join(lines[-available_height:])
|
354
|
+
md_obj.renderable = Markdown(truncated_content)
|
355
|
+
else:
|
356
|
+
md_obj.renderable = Markdown(content)
|
349
357
|
else:
|
350
358
|
md_obj.renderable = Markdown(content)
|
351
|
-
|
352
|
-
|
359
|
+
|
360
|
+
live.update(md_obj)
|
353
361
|
|
354
|
-
|
362
|
+
# Ensure the display refreshes with new content
|
363
|
+
live.refresh()
|
355
364
|
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
# Fallback if something goes wrong
|
366
|
-
sys.stderr.write(f"\nError stopping live display: {str(e)}\n")
|
367
|
-
sys.stderr.flush()
|
365
|
+
# If streaming is complete, stop the live display
|
366
|
+
if is_complete:
|
367
|
+
try:
|
368
|
+
# Just stop the live display when complete - no need to redisplay content
|
369
|
+
live.stop()
|
370
|
+
except Exception as e:
|
371
|
+
# Fallback if something goes wrong
|
372
|
+
sys.stderr.write(f"\nError stopping live display: {str(e)}\n")
|
373
|
+
sys.stderr.flush()
|
368
374
|
|
369
375
|
# Define a function to set up and start the spinner
|
370
376
|
def setup_spinner(stop_event, message="Waiting for AI response...", color=COLORS['cyan']):
|
ngpt/cli/roles.py
ADDED
@@ -0,0 +1,265 @@
|
|
1
|
+
import os
|
2
|
+
import json
|
3
|
+
import sys
|
4
|
+
from pathlib import Path
|
5
|
+
from .formatters import COLORS
|
6
|
+
from .ui import get_multiline_input
|
7
|
+
|
8
|
+
# Role directory within config
|
9
|
+
ROLE_DIR_NAME = "ngpt_roles"
|
10
|
+
|
11
|
+
def get_role_directory():
|
12
|
+
"""Get the path to the role directory, creating it if it doesn't exist."""
|
13
|
+
# Use XDG Base Directory specification if possible
|
14
|
+
if os.environ.get("XDG_CONFIG_HOME"):
|
15
|
+
config_dir = Path(os.environ["XDG_CONFIG_HOME"]) / "ngpt"
|
16
|
+
else:
|
17
|
+
config_dir = Path.home() / ".config" / "ngpt"
|
18
|
+
|
19
|
+
# Create role directory if it doesn't exist
|
20
|
+
role_dir = config_dir / ROLE_DIR_NAME
|
21
|
+
role_dir.mkdir(parents=True, exist_ok=True)
|
22
|
+
|
23
|
+
return role_dir
|
24
|
+
|
25
|
+
def create_role(role_name):
|
26
|
+
"""Create a new role with the given name.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
role_name: The name of the role to create.
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
bool: True if the role was created successfully, False otherwise.
|
33
|
+
"""
|
34
|
+
role_dir = get_role_directory()
|
35
|
+
role_file = role_dir / f"{role_name}.json"
|
36
|
+
|
37
|
+
# Check if role already exists
|
38
|
+
if role_file.exists():
|
39
|
+
print(f"{COLORS['yellow']}Role '{role_name}' already exists. Use --role-config edit {role_name} to modify it.{COLORS['reset']}")
|
40
|
+
return False
|
41
|
+
|
42
|
+
print(f"Creating new role '{role_name}'. Enter system prompt below (Ctrl+D to finish):")
|
43
|
+
|
44
|
+
# Get multiline input for the system prompt
|
45
|
+
system_prompt = get_multiline_input()
|
46
|
+
if not system_prompt:
|
47
|
+
print(f"{COLORS['yellow']}Role creation cancelled.{COLORS['reset']}")
|
48
|
+
return False
|
49
|
+
|
50
|
+
# Create role data
|
51
|
+
role_data = {
|
52
|
+
"name": role_name,
|
53
|
+
"system_prompt": system_prompt
|
54
|
+
}
|
55
|
+
|
56
|
+
# Save role to file
|
57
|
+
try:
|
58
|
+
with open(role_file, 'w') as f:
|
59
|
+
json.dump(role_data, f, indent=2)
|
60
|
+
print(f"{COLORS['green']}Role '{role_name}' created successfully.{COLORS['reset']}")
|
61
|
+
return True
|
62
|
+
except Exception as e:
|
63
|
+
print(f"{COLORS['red']}Error creating role: {str(e)}{COLORS['reset']}")
|
64
|
+
return False
|
65
|
+
|
66
|
+
def edit_role(role_name):
|
67
|
+
"""Edit an existing role with the given name.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
role_name: The name of the role to edit.
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
bool: True if the role was edited successfully, False otherwise.
|
74
|
+
"""
|
75
|
+
role_dir = get_role_directory()
|
76
|
+
role_file = role_dir / f"{role_name}.json"
|
77
|
+
|
78
|
+
# Check if role exists
|
79
|
+
if not role_file.exists():
|
80
|
+
print(f"{COLORS['yellow']}Role '{role_name}' does not exist.{COLORS['reset']}")
|
81
|
+
return False
|
82
|
+
|
83
|
+
# Load existing role data
|
84
|
+
try:
|
85
|
+
with open(role_file, 'r') as f:
|
86
|
+
role_data = json.load(f)
|
87
|
+
|
88
|
+
print(f"Editing role '{role_name}'. Current system prompt will be loaded in the editor.")
|
89
|
+
|
90
|
+
# Get multiline input for the new system prompt with the current one pre-loaded
|
91
|
+
system_prompt = get_multiline_input(initial_text=role_data['system_prompt'])
|
92
|
+
if not system_prompt:
|
93
|
+
print(f"{COLORS['yellow']}Role edit cancelled.{COLORS['reset']}")
|
94
|
+
return False
|
95
|
+
|
96
|
+
# Update role data
|
97
|
+
role_data['system_prompt'] = system_prompt
|
98
|
+
|
99
|
+
# Save updated role to file
|
100
|
+
with open(role_file, 'w') as f:
|
101
|
+
json.dump(role_data, f, indent=2)
|
102
|
+
|
103
|
+
print(f"{COLORS['green']}Role '{role_name}' updated successfully.{COLORS['reset']}")
|
104
|
+
return True
|
105
|
+
except Exception as e:
|
106
|
+
print(f"{COLORS['red']}Error editing role: {str(e)}{COLORS['reset']}")
|
107
|
+
return False
|
108
|
+
|
109
|
+
def show_role(role_name):
|
110
|
+
"""Show details of a role with the given name.
|
111
|
+
|
112
|
+
Args:
|
113
|
+
role_name: The name of the role to show.
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
bool: True if the role was found and displayed, False otherwise.
|
117
|
+
"""
|
118
|
+
role_dir = get_role_directory()
|
119
|
+
role_file = role_dir / f"{role_name}.json"
|
120
|
+
|
121
|
+
# Check if role exists
|
122
|
+
if not role_file.exists():
|
123
|
+
print(f"{COLORS['yellow']}Role '{role_name}' does not exist.{COLORS['reset']}")
|
124
|
+
return False
|
125
|
+
|
126
|
+
# Load role data
|
127
|
+
try:
|
128
|
+
with open(role_file, 'r') as f:
|
129
|
+
role_data = json.load(f)
|
130
|
+
|
131
|
+
print(f"\n{COLORS['bold']}Role: {COLORS['cyan']}{role_name}{COLORS['reset']}")
|
132
|
+
print(f"\n{COLORS['bold']}System Prompt:{COLORS['reset']}")
|
133
|
+
print(f"{COLORS['cyan']}{role_data['system_prompt']}{COLORS['reset']}")
|
134
|
+
|
135
|
+
return True
|
136
|
+
except Exception as e:
|
137
|
+
print(f"{COLORS['red']}Error showing role: {str(e)}{COLORS['reset']}")
|
138
|
+
return False
|
139
|
+
|
140
|
+
def list_roles():
|
141
|
+
"""List all available roles.
|
142
|
+
|
143
|
+
Returns:
|
144
|
+
bool: True if roles were listed successfully, False otherwise.
|
145
|
+
"""
|
146
|
+
role_dir = get_role_directory()
|
147
|
+
|
148
|
+
# Get all JSON files in the role directory
|
149
|
+
try:
|
150
|
+
role_files = list(role_dir.glob("*.json"))
|
151
|
+
|
152
|
+
if not role_files:
|
153
|
+
print(f"{COLORS['yellow']}No roles found. Use --role-config create <role_name> to create a new role.{COLORS['reset']}")
|
154
|
+
return True
|
155
|
+
|
156
|
+
print(f"\n{COLORS['bold']}Available Roles:{COLORS['reset']}")
|
157
|
+
for role_file in sorted(role_files):
|
158
|
+
role_name = role_file.stem
|
159
|
+
print(f" • {COLORS['cyan']}{role_name}{COLORS['reset']}")
|
160
|
+
|
161
|
+
return True
|
162
|
+
except Exception as e:
|
163
|
+
print(f"{COLORS['red']}Error listing roles: {str(e)}{COLORS['reset']}")
|
164
|
+
return False
|
165
|
+
|
166
|
+
def remove_role(role_name):
|
167
|
+
"""Remove a role with the given name.
|
168
|
+
|
169
|
+
Args:
|
170
|
+
role_name: The name of the role to remove.
|
171
|
+
|
172
|
+
Returns:
|
173
|
+
bool: True if the role was removed successfully, False otherwise.
|
174
|
+
"""
|
175
|
+
role_dir = get_role_directory()
|
176
|
+
role_file = role_dir / f"{role_name}.json"
|
177
|
+
|
178
|
+
# Check if role exists
|
179
|
+
if not role_file.exists():
|
180
|
+
print(f"{COLORS['yellow']}Role '{role_name}' does not exist.{COLORS['reset']}")
|
181
|
+
return False
|
182
|
+
|
183
|
+
# Confirm deletion
|
184
|
+
confirm = input(f"Are you sure you want to remove the role '{role_name}'? (y/N): ")
|
185
|
+
if confirm.lower() not in ["y", "yes"]:
|
186
|
+
print(f"{COLORS['yellow']}Role removal cancelled.{COLORS['reset']}")
|
187
|
+
return False
|
188
|
+
|
189
|
+
# Remove role file
|
190
|
+
try:
|
191
|
+
os.remove(role_file)
|
192
|
+
print(f"{COLORS['green']}Role '{role_name}' removed successfully.{COLORS['reset']}")
|
193
|
+
return True
|
194
|
+
except Exception as e:
|
195
|
+
print(f"{COLORS['red']}Error removing role: {str(e)}{COLORS['reset']}")
|
196
|
+
return False
|
197
|
+
|
198
|
+
def get_role_prompt(role_name):
|
199
|
+
"""Get the system prompt for a role with the given name.
|
200
|
+
|
201
|
+
Args:
|
202
|
+
role_name: The name of the role.
|
203
|
+
|
204
|
+
Returns:
|
205
|
+
str or None: The system prompt for the role, or None if the role does not exist.
|
206
|
+
"""
|
207
|
+
role_dir = get_role_directory()
|
208
|
+
role_file = role_dir / f"{role_name}.json"
|
209
|
+
|
210
|
+
# Check if role exists
|
211
|
+
if not role_file.exists():
|
212
|
+
print(f"{COLORS['yellow']}Role '{role_name}' does not exist.{COLORS['reset']}")
|
213
|
+
return None
|
214
|
+
|
215
|
+
# Load role data
|
216
|
+
try:
|
217
|
+
with open(role_file, 'r') as f:
|
218
|
+
role_data = json.load(f)
|
219
|
+
|
220
|
+
return role_data.get('system_prompt')
|
221
|
+
except Exception as e:
|
222
|
+
print(f"{COLORS['red']}Error loading role: {str(e)}{COLORS['reset']}")
|
223
|
+
return None
|
224
|
+
|
225
|
+
def show_help():
|
226
|
+
"""Show help information for role configuration."""
|
227
|
+
print(f"\n{COLORS['bold']}Role Configuration Help:{COLORS['reset']}")
|
228
|
+
print(f" {COLORS['cyan']}--role-config help{COLORS['reset']} - Show this help information")
|
229
|
+
print(f" {COLORS['cyan']}--role-config create <role_name>{COLORS['reset']} - Create a new role")
|
230
|
+
print(f" {COLORS['cyan']}--role-config show <role_name>{COLORS['reset']} - Show details of a role")
|
231
|
+
print(f" {COLORS['cyan']}--role-config edit <role_name>{COLORS['reset']} - Edit an existing role")
|
232
|
+
print(f" {COLORS['cyan']}--role-config list{COLORS['reset']} - List all available roles")
|
233
|
+
print(f" {COLORS['cyan']}--role-config remove <role_name>{COLORS['reset']} - Remove a role")
|
234
|
+
print(f"\n{COLORS['bold']}Usage Examples:{COLORS['reset']}")
|
235
|
+
print(f" {COLORS['cyan']}ngpt --role-config create json_generator{COLORS['reset']} - Create a new role for generating JSON")
|
236
|
+
print(f" {COLORS['cyan']}ngpt --role json_generator \"generate random user data\"{COLORS['reset']} - Use the json_generator role")
|
237
|
+
|
238
|
+
def handle_role_config(action, role_name):
|
239
|
+
"""Handle role configuration based on the action and role name.
|
240
|
+
|
241
|
+
Args:
|
242
|
+
action: The action to perform (help, create, show, edit, list, remove).
|
243
|
+
role_name: The name of the role (or None for actions like list and help).
|
244
|
+
|
245
|
+
Returns:
|
246
|
+
bool: True if the action was handled successfully, False otherwise.
|
247
|
+
"""
|
248
|
+
if action == "help":
|
249
|
+
show_help()
|
250
|
+
return True
|
251
|
+
elif action == "create":
|
252
|
+
return create_role(role_name)
|
253
|
+
elif action == "show":
|
254
|
+
return show_role(role_name)
|
255
|
+
elif action == "edit":
|
256
|
+
return edit_role(role_name)
|
257
|
+
elif action == "list":
|
258
|
+
return list_roles()
|
259
|
+
elif action == "remove":
|
260
|
+
return remove_role(role_name)
|
261
|
+
else:
|
262
|
+
# This shouldn't happen due to prior validation
|
263
|
+
print(f"{COLORS['yellow']}Unknown action: {action}{COLORS['reset']}")
|
264
|
+
show_help()
|
265
|
+
return False
|
ngpt/cli/ui.py
CHANGED
@@ -22,9 +22,12 @@ try:
|
|
22
22
|
except ImportError:
|
23
23
|
HAS_PROMPT_TOOLKIT = False
|
24
24
|
|
25
|
-
def create_multiline_editor():
|
25
|
+
def create_multiline_editor(initial_text=None):
|
26
26
|
"""Create a multi-line editor with prompt_toolkit.
|
27
27
|
|
28
|
+
Args:
|
29
|
+
initial_text: Optional initial text to prepopulate the editor with
|
30
|
+
|
28
31
|
Returns:
|
29
32
|
tuple: (app, has_prompt_toolkit) - the editor application and a boolean
|
30
33
|
indicating if prompt_toolkit is available
|
@@ -62,6 +65,7 @@ def create_multiline_editor():
|
|
62
65
|
scrollbar=True,
|
63
66
|
focus_on_click=True,
|
64
67
|
lexer=None,
|
68
|
+
text=initial_text or "", # Set initial text if provided
|
65
69
|
)
|
66
70
|
text_area.window.right_margins = [ScrollbarMargin(display_arrows=True)]
|
67
71
|
|
@@ -108,13 +112,16 @@ def create_multiline_editor():
|
|
108
112
|
print(f"Error creating editor: {e}")
|
109
113
|
return None, False
|
110
114
|
|
111
|
-
def get_multiline_input():
|
115
|
+
def get_multiline_input(initial_text=None):
|
112
116
|
"""Get multi-line input from the user using either prompt_toolkit or standard input.
|
113
117
|
|
118
|
+
Args:
|
119
|
+
initial_text: Optional initial text to prepopulate the editor with
|
120
|
+
|
114
121
|
Returns:
|
115
122
|
str: The user's input text, or None if cancelled
|
116
123
|
"""
|
117
|
-
editor_app, has_editor = create_multiline_editor()
|
124
|
+
editor_app, has_editor = create_multiline_editor(initial_text)
|
118
125
|
|
119
126
|
if has_editor and editor_app:
|
120
127
|
try:
|
@@ -132,8 +139,14 @@ def get_multiline_input():
|
|
132
139
|
if not HAS_PROMPT_TOOLKIT:
|
133
140
|
print("Note: Install 'prompt_toolkit' package for an enhanced input experience")
|
134
141
|
|
142
|
+
# Show initial text if provided
|
143
|
+
if initial_text:
|
144
|
+
print(initial_text)
|
145
|
+
lines = initial_text.splitlines()
|
146
|
+
else:
|
147
|
+
lines = []
|
148
|
+
|
135
149
|
# Use a more robust approach for multiline input without prompt_toolkit
|
136
|
-
lines = []
|
137
150
|
try:
|
138
151
|
while True:
|
139
152
|
try:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ngpt
|
3
|
-
Version: 3.
|
3
|
+
Version: 3.9.0
|
4
4
|
Summary: Swiss army knife for LLMs: powerful CLI and interactive chatbot in one package. Seamlessly work with OpenAI, Ollama, Groq, Claude, Gemini, or any OpenAI-compatible API to generate code, craft git commits, rewrite text, and execute shell commands.
|
5
5
|
Project-URL: Homepage, https://github.com/nazdridoy/ngpt
|
6
6
|
Project-URL: Repository, https://github.com/nazdridoy/ngpt
|
@@ -75,6 +75,7 @@ Description-Content-Type: text/markdown
|
|
75
75
|
- 📝 **Rich Multiline Editor**: Interactive multiline text input with syntax highlighting and intuitive controls
|
76
76
|
- 📑 **Git Commit Messages**: AI-powered generation of conventional, detailed commit messages from git diffs
|
77
77
|
- 🎭 **System Prompts**: Customize model behavior with custom system prompts
|
78
|
+
- 🤖 **Custom Roles**: Create and use reusable AI roles for specialized tasks
|
78
79
|
- 📃 **Conversation Logging**: Save your conversations to text files for later reference
|
79
80
|
- 🔌 **Modular Architecture**: Well-structured codebase with clean separation of concerns
|
80
81
|
- 🔄 **Provider Switching**: Easily switch between different LLM providers with a single parameter
|
@@ -164,6 +165,12 @@ What is the best way to learn Golang?
|
|
164
165
|
Provide simple hello world example.
|
165
166
|
EOF
|
166
167
|
|
168
|
+
# Create a custom role for specialized tasks
|
169
|
+
ngpt --role-config create json_generator
|
170
|
+
|
171
|
+
# Use a custom role for specific tasks
|
172
|
+
ngpt --role json_generator "Generate user data with name, email, and address"
|
173
|
+
|
167
174
|
# Rewrite text to improve quality while preserving tone and meaning
|
168
175
|
echo "your text" | ngpt -r
|
169
176
|
|
@@ -267,10 +274,11 @@ For more examples and detailed usage, visit the [CLI Usage Guide](https://nazdri
|
|
267
274
|
|
268
275
|
usage: ngpt [-h] [-v] [--language LANGUAGE] [--config [CONFIG]] [--config-index CONFIG_INDEX] [--provider PROVIDER]
|
269
276
|
[--remove] [--show-config] [--all] [--list-models] [--list-renderers] [--cli-config [COMMAND ...]]
|
270
|
-
[--api-key API_KEY] [--base-url BASE_URL] [--model MODEL] [--web-search]
|
271
|
-
[--temperature TEMPERATURE] [--top_p TOP_P] [--max_tokens MAX_TOKENS] [--log [FILE]]
|
272
|
-
[--no-stream | --prettify | --stream-prettify]
|
273
|
-
[--
|
277
|
+
[--role-config [ACTION ...]] [--api-key API_KEY] [--base-url BASE_URL] [--model MODEL] [--web-search]
|
278
|
+
[--pipe] [--temperature TEMPERATURE] [--top_p TOP_P] [--max_tokens MAX_TOKENS] [--log [FILE]]
|
279
|
+
[--preprompt PREPROMPT | --role ROLE] [--no-stream | --prettify | --stream-prettify]
|
280
|
+
[--renderer {auto,rich,glow}] [--rec-chunk] [--diff [FILE]] [--chunk-size CHUNK_SIZE]
|
281
|
+
[--analyses-chunk-size ANALYSES_CHUNK_SIZE] [--max-msg-lines MAX_MSG_LINES]
|
274
282
|
[--max-recursion-depth MAX_RECURSION_DEPTH] [-i | -s | -c | -t | -r | -g]
|
275
283
|
[prompt]
|
276
284
|
|
@@ -297,6 +305,7 @@ Configuration Options::
|
|
297
305
|
--list-models List all available models for the current configuration and exit
|
298
306
|
--list-renderers Show available markdown renderers for use with --prettify
|
299
307
|
--cli-config [COMMAND ...] Manage CLI configuration (set, get, unset, list, help)
|
308
|
+
--role-config [ACTION ...] Manage custom roles (help, create, show, edit, list, remove) [role_name]
|
300
309
|
|
301
310
|
Global Options::
|
302
311
|
|
@@ -310,6 +319,7 @@ Global Options::
|
|
310
319
|
--max_tokens MAX_TOKENS Set max response length in tokens
|
311
320
|
--log [FILE] Set filepath to log conversation to, or create a temporary log file if no path provided
|
312
321
|
--preprompt PREPROMPT Set custom system prompt to control AI behavior
|
322
|
+
--role ROLE Use a predefined role to set system prompt (mutually exclusive with --preprompt)
|
313
323
|
--renderer {auto,rich,glow} Select which markdown renderer to use with --prettify or --stream-prettify (auto, rich, or glow)
|
314
324
|
|
315
325
|
Output Display Options (mutually exclusive)::
|
@@ -325,7 +335,7 @@ Git Commit Message Options::
|
|
325
335
|
--chunk-size CHUNK_SIZE Number of lines per chunk when chunking is enabled (default: 200)
|
326
336
|
--analyses-chunk-size ANALYSES_CHUNK_SIZE Number of lines per chunk when recursively chunking analyses (default: 200)
|
327
337
|
--max-msg-lines MAX_MSG_LINES Maximum number of lines in commit message before condensing (default: 20)
|
328
|
-
--max-recursion-depth MAX_RECURSION_DEPTH
|
338
|
+
--max-recursion-depth MAX_RECURSION_DEPTH Maximum recursion depth for commit message condensing (default: 3)
|
329
339
|
|
330
340
|
Modes (mutually exclusive)::
|
331
341
|
|
@@ -601,6 +611,40 @@ This is a huge time-saver. nGPT analyzes your git diff and generates a properly
|
|
601
611
|
|
602
612
|

|
603
613
|
|
614
|
+
#### Custom AI Roles
|
615
|
+
|
616
|
+
```bash
|
617
|
+
# Create a specialized role for JSON generation
|
618
|
+
ngpt --role-config create json_generator
|
619
|
+
|
620
|
+
# Use the custom role to generate structured data
|
621
|
+
ngpt --role json_generator "Generate random user profile data"
|
622
|
+
```
|
623
|
+
```json
|
624
|
+
{
|
625
|
+
"id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
|
626
|
+
"firstName": "Aurora",
|
627
|
+
"lastName": "Reynolds",
|
628
|
+
"email": "aurora.reynolds@example.com",
|
629
|
+
"phone": "+1-555-0101",
|
630
|
+
"address": {
|
631
|
+
"street": "123 Main St",
|
632
|
+
"city": "Anytown",
|
633
|
+
"state": "CA",
|
634
|
+
"zipCode": "90210"
|
635
|
+
},
|
636
|
+
"birthDate": "1990-07-15",
|
637
|
+
"registrationDate": "2022-01-20",
|
638
|
+
"isActive": true,
|
639
|
+
"roles": [
|
640
|
+
"user",
|
641
|
+
"premium"
|
642
|
+
]
|
643
|
+
}
|
644
|
+
```
|
645
|
+
|
646
|
+
Custom roles let you define specialized AI personas that you can reuse across different prompts, making it easy to get consistent responses for specific tasks.
|
647
|
+
|
604
648
|
#### Web Search Integration
|
605
649
|
|
606
650
|
```bash
|
@@ -0,0 +1,30 @@
|
|
1
|
+
ngpt/__init__.py,sha256=kpKhViLakwMdHZkuLht2vWcjt0uD_5gR33gvMhfXr6w,664
|
2
|
+
ngpt/__main__.py,sha256=j3eFYPOtCCFBOGh7NK5IWEnADnTMMSEB9GLyIDoW724,66
|
3
|
+
ngpt/client.py,sha256=XjpA2UnvrRvzk6_DzVEddUTzoPlF8koQ-cZURpHoT7c,9041
|
4
|
+
ngpt/cli/__init__.py,sha256=hebbDSMGiOd43YNnQP67uzr67Ue6rZPwm2czynr5iZY,43
|
5
|
+
ngpt/cli/args.py,sha256=TbaID-NgRTCa1NQAct74XqWTADvSMbf-sSXKNI1NkXU,14537
|
6
|
+
ngpt/cli/config_manager.py,sha256=NQQcWnjUppAAd0s0p9YAf8EyKS1ex5-0EB4DvKdB4dk,3662
|
7
|
+
ngpt/cli/formatters.py,sha256=HBYGlx_7eoAKyzfy0Vq5L0yn8yVKjngqYBukMmXCcz0,9401
|
8
|
+
ngpt/cli/main.py,sha256=36mi8uYDcl56IhTkt-TJTRRhwHeF157xMAYgufLRAMo,29256
|
9
|
+
ngpt/cli/renderers.py,sha256=vAoDkpvgG2Fl81zkJDk_-zM1Fsw8E4Uv6m1AI81Fawo,17049
|
10
|
+
ngpt/cli/roles.py,sha256=YDBlmMtgw1gooeHVBiEUHwKn1V3E9dJKO8cpKoXu8uo,9359
|
11
|
+
ngpt/cli/ui.py,sha256=8-WyPMwgQiqLXWO0mGfBhKTRnIDDtPUtm_XCvOnqBJA,11334
|
12
|
+
ngpt/cli/modes/__init__.py,sha256=KP7VR6Xw9k1p5Jcu0F38RDxSFvFIzH3j1ThDLNwznUI,363
|
13
|
+
ngpt/cli/modes/chat.py,sha256=x1leClKq7UupA_CdW4tym0AivY2o_II123-I5IcAkxQ,7091
|
14
|
+
ngpt/cli/modes/code.py,sha256=Qj59xq6fZqgUDw7SbvmPKX_gdpc7DHJhNkn1sB5qgUU,12932
|
15
|
+
ngpt/cli/modes/gitcommsg.py,sha256=iTg3KlZwI0lGMcmUa62b0ashwLcxegdEEvT29PPtpBc,49595
|
16
|
+
ngpt/cli/modes/interactive.py,sha256=E0c38NA8xnuRKAce40F35uFYcohFDvaqSB8nf1ywS-4,17958
|
17
|
+
ngpt/cli/modes/rewrite.py,sha256=QQm453X9aoUQP9CAtmeghlMytMJPlsDZPKef9tGfj6g,11181
|
18
|
+
ngpt/cli/modes/shell.py,sha256=it1Brq1-LGeNfPKYBeVAwF-a78g9UP-KscofBZQkbr4,41589
|
19
|
+
ngpt/cli/modes/text.py,sha256=rUr7Byds7zkO9ZbMSZWOO_3fWfpeM40Tq1XJAgdapcs,6679
|
20
|
+
ngpt/utils/__init__.py,sha256=_92f8eGMMOtQQA3uwgSRVwUEl1EIRFjWPUjcfGgI-eI,1244
|
21
|
+
ngpt/utils/cli_config.py,sha256=Ug8cECBTIuzOwkBWidLTfs-OAdOsCMJ2bNa70pOADfw,11195
|
22
|
+
ngpt/utils/config.py,sha256=wsArA4osnh8fKqOvtsPqqBxAz3DpdjtaWUFaRtnUdyc,10452
|
23
|
+
ngpt/utils/log.py,sha256=f1jg2iFo35PAmsarH8FVL_62plq4VXH0Mu2QiP6RJGw,15934
|
24
|
+
ngpt/utils/pipe.py,sha256=qRHF-Ma7bbU0cOcb1Yhe4S-kBavivtnnvLA3EYS4FY4,2162
|
25
|
+
ngpt/utils/web_search.py,sha256=w5ke4KJMRxq7r5jtbUXvspja6XhjoPZloVkZ0IvBXIE,30731
|
26
|
+
ngpt-3.9.0.dist-info/METADATA,sha256=wf1tVRaKH7xODB9yrI1eO4STYMskdwq-VhYl0edL5N0,30309
|
27
|
+
ngpt-3.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
28
|
+
ngpt-3.9.0.dist-info/entry_points.txt,sha256=SqAAvLhMrsEpkIr4YFRdUeyuXQ9o0IBCeYgE6AVojoI,44
|
29
|
+
ngpt-3.9.0.dist-info/licenses/LICENSE,sha256=mQkpWoADxbHqE0HRefYLJdm7OpdrXBr3vNv5bZ8w72M,1065
|
30
|
+
ngpt-3.9.0.dist-info/RECORD,,
|
ngpt-3.8.2.dist-info/RECORD
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
ngpt/__init__.py,sha256=kpKhViLakwMdHZkuLht2vWcjt0uD_5gR33gvMhfXr6w,664
|
2
|
-
ngpt/__main__.py,sha256=j3eFYPOtCCFBOGh7NK5IWEnADnTMMSEB9GLyIDoW724,66
|
3
|
-
ngpt/client.py,sha256=XjpA2UnvrRvzk6_DzVEddUTzoPlF8koQ-cZURpHoT7c,9041
|
4
|
-
ngpt/cli/__init__.py,sha256=hebbDSMGiOd43YNnQP67uzr67Ue6rZPwm2czynr5iZY,43
|
5
|
-
ngpt/cli/args.py,sha256=HYCDHhqP-BI_tibL1qGQ9we4483h_kCa2ksh-QxOeiU,12694
|
6
|
-
ngpt/cli/config_manager.py,sha256=NQQcWnjUppAAd0s0p9YAf8EyKS1ex5-0EB4DvKdB4dk,3662
|
7
|
-
ngpt/cli/formatters.py,sha256=HBYGlx_7eoAKyzfy0Vq5L0yn8yVKjngqYBukMmXCcz0,9401
|
8
|
-
ngpt/cli/main.py,sha256=PVulo8Pm53-oQ2Pgc4G90YhwyPImt8j7HKmY38SJ7CM,28696
|
9
|
-
ngpt/cli/renderers.py,sha256=m71BeUXKynpKKGXFzwRSW1XngvyKiZ_xEsdujUbU0MA,16597
|
10
|
-
ngpt/cli/ui.py,sha256=4RFxIf51di5EsytVr7OoyCWF_d40KJ0Mbom0VWgPlCc,10870
|
11
|
-
ngpt/cli/modes/__init__.py,sha256=KP7VR6Xw9k1p5Jcu0F38RDxSFvFIzH3j1ThDLNwznUI,363
|
12
|
-
ngpt/cli/modes/chat.py,sha256=1mH3nTDwU52qQJRliNUAFSqJWyyiJ6j0T5uVso-VnxE,6828
|
13
|
-
ngpt/cli/modes/code.py,sha256=QBFgMRPKJhxkYCmumoSkNSF15XNRGUDum5yuK8aBnyM,12662
|
14
|
-
ngpt/cli/modes/gitcommsg.py,sha256=iTg3KlZwI0lGMcmUa62b0ashwLcxegdEEvT29PPtpBc,49595
|
15
|
-
ngpt/cli/modes/interactive.py,sha256=TtBrZUX45CVfKOPvkb1ya7dIQhXLILtn7ajmfM9ohso,17419
|
16
|
-
ngpt/cli/modes/rewrite.py,sha256=yPmJPPkMHNxrnV-eoM0j6lMNRhdSAMXmcw2s9xG6TIo,10918
|
17
|
-
ngpt/cli/modes/shell.py,sha256=nXkUbLLNdXQrdsOqGXMwdfnr2y7MtqbmBkYlwZbI5JA,41415
|
18
|
-
ngpt/cli/modes/text.py,sha256=7t5WWXMFxGkBM5HMP4irbN9aQwxE2YgywjiVPep710k,6417
|
19
|
-
ngpt/utils/__init__.py,sha256=_92f8eGMMOtQQA3uwgSRVwUEl1EIRFjWPUjcfGgI-eI,1244
|
20
|
-
ngpt/utils/cli_config.py,sha256=Ug8cECBTIuzOwkBWidLTfs-OAdOsCMJ2bNa70pOADfw,11195
|
21
|
-
ngpt/utils/config.py,sha256=wsArA4osnh8fKqOvtsPqqBxAz3DpdjtaWUFaRtnUdyc,10452
|
22
|
-
ngpt/utils/log.py,sha256=f1jg2iFo35PAmsarH8FVL_62plq4VXH0Mu2QiP6RJGw,15934
|
23
|
-
ngpt/utils/pipe.py,sha256=qRHF-Ma7bbU0cOcb1Yhe4S-kBavivtnnvLA3EYS4FY4,2162
|
24
|
-
ngpt/utils/web_search.py,sha256=w5ke4KJMRxq7r5jtbUXvspja6XhjoPZloVkZ0IvBXIE,30731
|
25
|
-
ngpt-3.8.2.dist-info/METADATA,sha256=2LazTO2smhA2gBILZQ9vA0Xqqn2RByEIfclp999Y6KA,28919
|
26
|
-
ngpt-3.8.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
27
|
-
ngpt-3.8.2.dist-info/entry_points.txt,sha256=SqAAvLhMrsEpkIr4YFRdUeyuXQ9o0IBCeYgE6AVojoI,44
|
28
|
-
ngpt-3.8.2.dist-info/licenses/LICENSE,sha256=mQkpWoADxbHqE0HRefYLJdm7OpdrXBr3vNv5bZ8w72M,1065
|
29
|
-
ngpt-3.8.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|