ngpt 1.5.1__py3-none-any.whl → 1.6.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.py +185 -136
- {ngpt-1.5.1.dist-info → ngpt-1.6.0.dist-info}/METADATA +9 -1
- ngpt-1.6.0.dist-info/RECORD +9 -0
- ngpt-1.5.1.dist-info/RECORD +0 -9
- {ngpt-1.5.1.dist-info → ngpt-1.6.0.dist-info}/WHEEL +0 -0
- {ngpt-1.5.1.dist-info → ngpt-1.6.0.dist-info}/entry_points.txt +0 -0
- {ngpt-1.5.1.dist-info → ngpt-1.6.0.dist-info}/licenses/LICENSE +0 -0
ngpt/cli.py
CHANGED
@@ -18,6 +18,7 @@ try:
|
|
18
18
|
from prompt_toolkit.widgets import TextArea
|
19
19
|
from prompt_toolkit.layout.margins import ScrollbarMargin
|
20
20
|
from prompt_toolkit.filters import to_filter
|
21
|
+
from prompt_toolkit.history import InMemoryHistory
|
21
22
|
import shutil
|
22
23
|
HAS_PROMPT_TOOLKIT = True
|
23
24
|
except ImportError:
|
@@ -82,6 +83,183 @@ def check_config(config):
|
|
82
83
|
|
83
84
|
return True
|
84
85
|
|
86
|
+
def interactive_chat_session(client, web_search=False, no_stream=False):
|
87
|
+
"""Run an interactive chat session with conversation history."""
|
88
|
+
# Define ANSI color codes for terminal output
|
89
|
+
COLORS = {
|
90
|
+
"reset": "\033[0m",
|
91
|
+
"bold": "\033[1m",
|
92
|
+
"cyan": "\033[36m",
|
93
|
+
"green": "\033[32m",
|
94
|
+
"yellow": "\033[33m",
|
95
|
+
"blue": "\033[34m",
|
96
|
+
"magenta": "\033[35m",
|
97
|
+
"gray": "\033[90m",
|
98
|
+
"bg_blue": "\033[44m",
|
99
|
+
"bg_cyan": "\033[46m"
|
100
|
+
}
|
101
|
+
|
102
|
+
# Get terminal width for better formatting
|
103
|
+
try:
|
104
|
+
term_width = shutil.get_terminal_size().columns
|
105
|
+
except:
|
106
|
+
term_width = 80 # Default fallback
|
107
|
+
|
108
|
+
# Improved visual header with better layout
|
109
|
+
header = f"{COLORS['cyan']}{COLORS['bold']}🤖 nGPT Interactive Chat Session 🤖{COLORS['reset']}"
|
110
|
+
print(f"\n{header}")
|
111
|
+
|
112
|
+
# Create a separator line - use a consistent separator length for all lines
|
113
|
+
separator_length = min(40, term_width - 10)
|
114
|
+
separator = f"{COLORS['gray']}{'─' * separator_length}{COLORS['reset']}"
|
115
|
+
print(separator)
|
116
|
+
|
117
|
+
# Group commands into categories with better formatting
|
118
|
+
print(f"\n{COLORS['cyan']}Navigation:{COLORS['reset']}")
|
119
|
+
print(f" {COLORS['yellow']}↑/↓{COLORS['reset']} : Browse input history")
|
120
|
+
|
121
|
+
print(f"\n{COLORS['cyan']}Session Commands:{COLORS['reset']}")
|
122
|
+
print(f" {COLORS['yellow']}history{COLORS['reset']} : Show conversation history")
|
123
|
+
print(f" {COLORS['yellow']}clear{COLORS['reset']} : Reset conversation")
|
124
|
+
print(f" {COLORS['yellow']}exit{COLORS['reset']} : End session")
|
125
|
+
|
126
|
+
print(f"\n{separator}\n")
|
127
|
+
|
128
|
+
# Custom separator - use the same length for consistency
|
129
|
+
def print_separator():
|
130
|
+
print(f"\n{separator}\n")
|
131
|
+
|
132
|
+
# Initialize conversation history
|
133
|
+
system_prompt = "You are a helpful assistant."
|
134
|
+
conversation = []
|
135
|
+
system_message = {"role": "system", "content": system_prompt}
|
136
|
+
conversation.append(system_message)
|
137
|
+
|
138
|
+
# Initialize prompt_toolkit history
|
139
|
+
prompt_history = InMemoryHistory() if HAS_PROMPT_TOOLKIT else None
|
140
|
+
|
141
|
+
# Decorative chat headers with rounded corners
|
142
|
+
def user_header():
|
143
|
+
return f"{COLORS['cyan']}{COLORS['bold']}╭─ 👤 You {COLORS['reset']}"
|
144
|
+
|
145
|
+
def ngpt_header():
|
146
|
+
return f"{COLORS['green']}{COLORS['bold']}╭─ 🤖 nGPT {COLORS['reset']}"
|
147
|
+
|
148
|
+
# Function to display conversation history
|
149
|
+
def display_history():
|
150
|
+
if len(conversation) <= 1: # Only system message
|
151
|
+
print(f"\n{COLORS['yellow']}No conversation history yet.{COLORS['reset']}")
|
152
|
+
return
|
153
|
+
|
154
|
+
print(f"\n{COLORS['cyan']}{COLORS['bold']}Conversation History:{COLORS['reset']}")
|
155
|
+
print(separator)
|
156
|
+
|
157
|
+
# Skip system message
|
158
|
+
message_count = 0
|
159
|
+
for i, msg in enumerate(conversation):
|
160
|
+
if msg["role"] == "system":
|
161
|
+
continue
|
162
|
+
|
163
|
+
if msg["role"] == "user":
|
164
|
+
message_count += 1
|
165
|
+
print(f"\n{user_header()}")
|
166
|
+
print(f"{COLORS['cyan']}│ [{message_count}] {COLORS['reset']}{msg['content']}")
|
167
|
+
elif msg["role"] == "assistant":
|
168
|
+
print(f"\n{ngpt_header()}")
|
169
|
+
print(f"{COLORS['green']}│ {COLORS['reset']}{msg['content']}")
|
170
|
+
|
171
|
+
print(f"\n{separator}") # Consistent separator at the end
|
172
|
+
|
173
|
+
# Function to clear conversation history
|
174
|
+
def clear_history():
|
175
|
+
nonlocal conversation
|
176
|
+
conversation = [{"role": "system", "content": system_prompt}]
|
177
|
+
print(f"\n{COLORS['yellow']}Conversation history cleared.{COLORS['reset']}")
|
178
|
+
print(separator) # Add separator for consistency
|
179
|
+
|
180
|
+
try:
|
181
|
+
while True:
|
182
|
+
# Get user input
|
183
|
+
if HAS_PROMPT_TOOLKIT:
|
184
|
+
# Custom styling for prompt_toolkit
|
185
|
+
style = Style.from_dict({
|
186
|
+
'prompt': 'ansicyan bold',
|
187
|
+
'input': 'ansiwhite',
|
188
|
+
})
|
189
|
+
|
190
|
+
# Create key bindings for Ctrl+C handling
|
191
|
+
kb = KeyBindings()
|
192
|
+
@kb.add('c-c')
|
193
|
+
def _(event):
|
194
|
+
event.app.exit(result=None)
|
195
|
+
raise KeyboardInterrupt()
|
196
|
+
|
197
|
+
# Get user input with styled prompt - using proper HTML formatting
|
198
|
+
user_input = pt_prompt(
|
199
|
+
HTML("<ansicyan><b>╭─ 👤 You:</b></ansicyan> "),
|
200
|
+
style=style,
|
201
|
+
key_bindings=kb,
|
202
|
+
history=prompt_history
|
203
|
+
)
|
204
|
+
else:
|
205
|
+
user_input = input(f"{user_header()}: {COLORS['reset']}")
|
206
|
+
|
207
|
+
# Check for exit commands
|
208
|
+
if user_input.lower() in ('exit', 'quit', 'bye'):
|
209
|
+
print(f"\n{COLORS['green']}Ending chat session. Goodbye!{COLORS['reset']}")
|
210
|
+
break
|
211
|
+
|
212
|
+
# Check for special commands
|
213
|
+
if user_input.lower() == 'history':
|
214
|
+
display_history()
|
215
|
+
continue
|
216
|
+
|
217
|
+
if user_input.lower() == 'clear':
|
218
|
+
clear_history()
|
219
|
+
continue
|
220
|
+
|
221
|
+
# Skip empty messages but don't raise an error
|
222
|
+
if not user_input.strip():
|
223
|
+
continue
|
224
|
+
|
225
|
+
# Add user message to conversation
|
226
|
+
user_message = {"role": "user", "content": user_input}
|
227
|
+
conversation.append(user_message)
|
228
|
+
|
229
|
+
# Print assistant indicator with formatting
|
230
|
+
if not no_stream:
|
231
|
+
print(f"\n{ngpt_header()}: {COLORS['reset']}", end="", flush=True)
|
232
|
+
else:
|
233
|
+
print(f"\n{ngpt_header()}: {COLORS['reset']}", flush=True)
|
234
|
+
|
235
|
+
# Get AI response with conversation history
|
236
|
+
response = client.chat(
|
237
|
+
prompt=user_input,
|
238
|
+
messages=conversation,
|
239
|
+
stream=not no_stream,
|
240
|
+
web_search=web_search
|
241
|
+
)
|
242
|
+
|
243
|
+
# Add AI response to conversation history
|
244
|
+
if response:
|
245
|
+
assistant_message = {"role": "assistant", "content": response}
|
246
|
+
conversation.append(assistant_message)
|
247
|
+
|
248
|
+
# Print response if not streamed
|
249
|
+
if no_stream:
|
250
|
+
print(response)
|
251
|
+
|
252
|
+
# Print separator between exchanges
|
253
|
+
print_separator()
|
254
|
+
|
255
|
+
except KeyboardInterrupt:
|
256
|
+
print(f"\n\n{COLORS['green']}Chat session ended by user. Goodbye!{COLORS['reset']}")
|
257
|
+
except Exception as e:
|
258
|
+
print(f"\n{COLORS['yellow']}Error during chat session: {str(e)}{COLORS['reset']}")
|
259
|
+
# Print traceback for debugging if it's a serious error
|
260
|
+
import traceback
|
261
|
+
traceback.print_exc()
|
262
|
+
|
85
263
|
def main():
|
86
264
|
parser = argparse.ArgumentParser(description="nGPT - A CLI tool for interacting with custom OpenAI API endpoints")
|
87
265
|
|
@@ -109,10 +287,11 @@ def main():
|
|
109
287
|
# Mode flags (mutually exclusive)
|
110
288
|
mode_group = parser.add_argument_group('Modes (mutually exclusive)')
|
111
289
|
mode_exclusive_group = mode_group.add_mutually_exclusive_group()
|
290
|
+
mode_exclusive_group.add_argument('-i', '--interactive', action='store_true', help='Start an interactive chat session')
|
112
291
|
mode_exclusive_group.add_argument('-s', '--shell', action='store_true', help='Generate and execute shell commands')
|
113
292
|
mode_exclusive_group.add_argument('-c', '--code', action='store_true', help='Generate code')
|
114
293
|
mode_exclusive_group.add_argument('-t', '--text', action='store_true', help='Enter multi-line text input (submit with Ctrl+D)')
|
115
|
-
|
294
|
+
# Note: --show-config is handled separately and implicitly acts as a mode
|
116
295
|
|
117
296
|
# Language option for code mode
|
118
297
|
parser.add_argument('--language', default="python", help='Programming language to generate code in (for code mode)')
|
@@ -229,7 +408,7 @@ def main():
|
|
229
408
|
|
230
409
|
return
|
231
410
|
|
232
|
-
#
|
411
|
+
# For interactive mode, we'll allow continuing without a specific prompt
|
233
412
|
if not args.prompt and not (args.shell or args.code or args.text or args.interactive):
|
234
413
|
parser.print_help()
|
235
414
|
return
|
@@ -243,7 +422,10 @@ def main():
|
|
243
422
|
|
244
423
|
try:
|
245
424
|
# Handle modes
|
246
|
-
if args.
|
425
|
+
if args.interactive:
|
426
|
+
# Interactive chat mode
|
427
|
+
interactive_chat_session(client, web_search=args.web_search, no_stream=args.no_stream)
|
428
|
+
elif args.shell:
|
247
429
|
if args.prompt is None:
|
248
430
|
try:
|
249
431
|
print("Enter shell command description: ", end='')
|
@@ -279,139 +461,6 @@ def main():
|
|
279
461
|
except subprocess.CalledProcessError as e:
|
280
462
|
print(f"\nError:\n{e.stderr}")
|
281
463
|
|
282
|
-
elif args.interactive:
|
283
|
-
# Interactive chat mode
|
284
|
-
conversation_history = []
|
285
|
-
print("\033[94m\033[1m" + "Interactive Chat Mode" + "\033[0m")
|
286
|
-
print("Type your messages and press Ctrl+D to send. Type 'exit', 'quit', or use Ctrl+C to exit.")
|
287
|
-
print("Type 'clear' to start a new conversation.")
|
288
|
-
if HAS_PROMPT_TOOLKIT:
|
289
|
-
print("Use arrow keys to navigate, Enter for new line in multi-line mode.")
|
290
|
-
print()
|
291
|
-
|
292
|
-
try:
|
293
|
-
while True:
|
294
|
-
try:
|
295
|
-
# Get user input with prompt_toolkit if available
|
296
|
-
if HAS_PROMPT_TOOLKIT:
|
297
|
-
# Create key bindings
|
298
|
-
kb = KeyBindings()
|
299
|
-
|
300
|
-
# Explicitly bind Ctrl+C to exit
|
301
|
-
@kb.add('c-c')
|
302
|
-
def _(event):
|
303
|
-
event.app.exit(result=None)
|
304
|
-
print("\nExiting interactive chat mode.")
|
305
|
-
sys.exit(130)
|
306
|
-
|
307
|
-
# Explicitly bind Ctrl+D to submit
|
308
|
-
@kb.add('c-d')
|
309
|
-
def _(event):
|
310
|
-
event.app.exit(result=event.app.current_buffer.text)
|
311
|
-
|
312
|
-
# Get terminal dimensions
|
313
|
-
term_width, term_height = shutil.get_terminal_size()
|
314
|
-
|
315
|
-
# Create a styled TextArea
|
316
|
-
text_area = TextArea(
|
317
|
-
style="class:input-area",
|
318
|
-
multiline=True,
|
319
|
-
wrap_lines=True,
|
320
|
-
width=term_width - 4,
|
321
|
-
height=min(10, term_height - 8),
|
322
|
-
prompt=HTML("<ansiblue>>>> </ansiblue>"),
|
323
|
-
scrollbar=True,
|
324
|
-
focus_on_click=True,
|
325
|
-
lexer=None,
|
326
|
-
)
|
327
|
-
text_area.window.right_margins = [ScrollbarMargin(display_arrows=True)]
|
328
|
-
|
329
|
-
# Create a title bar
|
330
|
-
title_bar = FormattedTextControl(
|
331
|
-
HTML("<style bg='ansiblue' fg='ansiwhite'><b> NGPT Interactive Chat </b></style>")
|
332
|
-
)
|
333
|
-
|
334
|
-
# Create a status bar with key bindings and commands info
|
335
|
-
status_bar = FormattedTextControl(
|
336
|
-
HTML("<ansiblue><b>Ctrl+D</b></ansiblue>: Submit | <ansiblue><b>Ctrl+C</b></ansiblue>: Exit | Type <ansiblue><b>clear</b></ansiblue> to start new conversation")
|
337
|
-
)
|
338
|
-
|
339
|
-
# Create the layout
|
340
|
-
layout = Layout(
|
341
|
-
HSplit([
|
342
|
-
Window(title_bar, height=1),
|
343
|
-
Window(height=1, char="-", style="class:separator"),
|
344
|
-
text_area,
|
345
|
-
Window(height=1, char="-", style="class:separator"),
|
346
|
-
Window(status_bar, height=1),
|
347
|
-
])
|
348
|
-
)
|
349
|
-
|
350
|
-
# Create a style
|
351
|
-
style = Style.from_dict({
|
352
|
-
"separator": "ansigray",
|
353
|
-
"input-area": "bg:ansiblack fg:ansiwhite",
|
354
|
-
"cursor": "bg:ansiwhite fg:ansiblack",
|
355
|
-
})
|
356
|
-
|
357
|
-
# Create and run the application
|
358
|
-
app = Application(
|
359
|
-
layout=layout,
|
360
|
-
full_screen=False,
|
361
|
-
key_bindings=kb,
|
362
|
-
style=style,
|
363
|
-
mouse_support=True,
|
364
|
-
)
|
365
|
-
|
366
|
-
user_input = app.run()
|
367
|
-
if user_input is None:
|
368
|
-
break
|
369
|
-
else:
|
370
|
-
# Fallback to standard input
|
371
|
-
user_input = input("\033[1m\033[94m>>> \033[0m")
|
372
|
-
|
373
|
-
# Handle special commands
|
374
|
-
if user_input is None:
|
375
|
-
break
|
376
|
-
elif user_input.lower() in ['exit', 'quit', 'q']:
|
377
|
-
print("Exiting interactive chat mode.")
|
378
|
-
break
|
379
|
-
elif user_input.lower() == 'clear':
|
380
|
-
print("Starting a new conversation.")
|
381
|
-
conversation_history = []
|
382
|
-
continue
|
383
|
-
elif not user_input.strip():
|
384
|
-
continue
|
385
|
-
|
386
|
-
# Add user message to conversation history
|
387
|
-
conversation_history.append({"role": "user", "content": user_input})
|
388
|
-
|
389
|
-
# Get response from the model
|
390
|
-
print("\033[90m" + "AI is thinking..." + "\033[0m")
|
391
|
-
response = client.chat(
|
392
|
-
prompt=user_input,
|
393
|
-
stream=not args.no_stream,
|
394
|
-
messages=conversation_history,
|
395
|
-
web_search=args.web_search
|
396
|
-
)
|
397
|
-
|
398
|
-
# Add assistant message to conversation history
|
399
|
-
conversation_history.append({"role": "assistant", "content": response})
|
400
|
-
|
401
|
-
# If no streaming, print the response
|
402
|
-
if args.no_stream and response:
|
403
|
-
print("\033[92m" + response + "\033[0m")
|
404
|
-
|
405
|
-
print() # Add spacing between exchanges
|
406
|
-
|
407
|
-
except KeyboardInterrupt:
|
408
|
-
print("\nExiting interactive chat mode.")
|
409
|
-
break
|
410
|
-
|
411
|
-
except Exception as e:
|
412
|
-
print(f"\nError in interactive mode: {e}")
|
413
|
-
return
|
414
|
-
|
415
464
|
elif args.code:
|
416
465
|
if args.prompt is None:
|
417
466
|
try:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ngpt
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.6.0
|
4
4
|
Summary: A lightweight Python CLI and library for interacting with OpenAI-compatible APIs, supporting both official and self-hosted LLM endpoints.
|
5
5
|
Project-URL: Homepage, https://github.com/nazdridoy/ngpt
|
6
6
|
Project-URL: Repository, https://github.com/nazdridoy/ngpt
|
@@ -64,6 +64,9 @@ pip install ngpt
|
|
64
64
|
# Chat with default settings
|
65
65
|
ngpt "Tell me about quantum computing"
|
66
66
|
|
67
|
+
# Start an interactive chat session with conversation memory
|
68
|
+
ngpt -i
|
69
|
+
|
67
70
|
# Return response without streaming
|
68
71
|
ngpt -n "Tell me about quantum computing"
|
69
72
|
|
@@ -82,6 +85,7 @@ ngpt --text
|
|
82
85
|
- ✅ **Dual Mode**: Use as a CLI tool or import as a Python library
|
83
86
|
- 🪶 **Lightweight**: Minimal dependencies (just `requests`)
|
84
87
|
- 🔄 **API Flexibility**: Works with OpenAI, Ollama, Groq, and any compatible endpoint
|
88
|
+
- 💬 **Interactive Chat**: Continuous conversation with memory in modern UI
|
85
89
|
- 📊 **Streaming Responses**: Real-time output for better user experience
|
86
90
|
- 🔍 **Web Search**: Integrated with compatible API endpoints
|
87
91
|
- ⚙️ **Multiple Configurations**: Cross-platform config system supporting different profiles
|
@@ -105,6 +109,9 @@ Requires Python 3.8 or newer.
|
|
105
109
|
# Basic chat (default mode)
|
106
110
|
ngpt "Hello, how are you?"
|
107
111
|
|
112
|
+
# Interactive chat session with conversation history
|
113
|
+
ngpt -i
|
114
|
+
|
108
115
|
# Show version information
|
109
116
|
ngpt -v
|
110
117
|
|
@@ -211,6 +218,7 @@ You can configure the client using the following options:
|
|
211
218
|
| `--remove` | Remove the configuration at the specified index (requires --config and --config-index) |
|
212
219
|
| `--show-config` | Show configuration details and exit |
|
213
220
|
| `--all` | Used with `--show-config` to display all configurations |
|
221
|
+
| `-i, --interactive` | Start an interactive chat session with stylish UI, conversation history, and special commands |
|
214
222
|
| `-s, --shell` | Generate and execute shell commands |
|
215
223
|
| `-c, --code` | Generate clean code output |
|
216
224
|
| `-t, --text` | Open interactive multiline editor for complex prompts |
|
@@ -0,0 +1,9 @@
|
|
1
|
+
ngpt/__init__.py,sha256=ehInP9w0MZlS1vZ1g6Cm4YE1ftmgF72CnEddQ3Le9n4,368
|
2
|
+
ngpt/cli.py,sha256=brSdM8uFBbVktbpK6CDAFEzmKzBKb47OSK1G9ludDYg,27564
|
3
|
+
ngpt/client.py,sha256=O0dPYeQCJlpWZWBBsroo-5UxeyBVwqC6o3Pm8lRnDiY,10329
|
4
|
+
ngpt/config.py,sha256=BF0G3QeiPma8l7EQyc37bR7LWZog7FHJQNe7uj9cr4w,6896
|
5
|
+
ngpt-1.6.0.dist-info/METADATA,sha256=hqt6-L2cmyQ_GgoYEOf-_MVUlrm6n1JoxzgAqmVgFkc,10416
|
6
|
+
ngpt-1.6.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
7
|
+
ngpt-1.6.0.dist-info/entry_points.txt,sha256=1cnAMujyy34DlOahrJg19lePSnb08bLbkUs_kVerqdk,39
|
8
|
+
ngpt-1.6.0.dist-info/licenses/LICENSE,sha256=mQkpWoADxbHqE0HRefYLJdm7OpdrXBr3vNv5bZ8w72M,1065
|
9
|
+
ngpt-1.6.0.dist-info/RECORD,,
|
ngpt-1.5.1.dist-info/RECORD
DELETED
@@ -1,9 +0,0 @@
|
|
1
|
-
ngpt/__init__.py,sha256=ehInP9w0MZlS1vZ1g6Cm4YE1ftmgF72CnEddQ3Le9n4,368
|
2
|
-
ngpt/cli.py,sha256=ZMBmNJfKiKAO88MecT7CDHH1uyqpupeCqGSX4-5LC_g,27117
|
3
|
-
ngpt/client.py,sha256=O0dPYeQCJlpWZWBBsroo-5UxeyBVwqC6o3Pm8lRnDiY,10329
|
4
|
-
ngpt/config.py,sha256=BF0G3QeiPma8l7EQyc37bR7LWZog7FHJQNe7uj9cr4w,6896
|
5
|
-
ngpt-1.5.1.dist-info/METADATA,sha256=eHORtsw7QD5HwfIhqRzCojOTNtfBqZ6kXCFT_8VBRq8,10086
|
6
|
-
ngpt-1.5.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
7
|
-
ngpt-1.5.1.dist-info/entry_points.txt,sha256=1cnAMujyy34DlOahrJg19lePSnb08bLbkUs_kVerqdk,39
|
8
|
-
ngpt-1.5.1.dist-info/licenses/LICENSE,sha256=mQkpWoADxbHqE0HRefYLJdm7OpdrXBr3vNv5bZ8w72M,1065
|
9
|
-
ngpt-1.5.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|