chatrepl 0.1.0a0__tar.gz
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.
Potentially problematic release.
This version of chatrepl might be problematic. Click here for more details.
- chatrepl-0.1.0a0/LICENSE +21 -0
- chatrepl-0.1.0a0/PKG-INFO +109 -0
- chatrepl-0.1.0a0/README.md +93 -0
- chatrepl-0.1.0a0/chatrepl.egg-info/PKG-INFO +109 -0
- chatrepl-0.1.0a0/chatrepl.egg-info/SOURCES.txt +9 -0
- chatrepl-0.1.0a0/chatrepl.egg-info/dependency_links.txt +1 -0
- chatrepl-0.1.0a0/chatrepl.egg-info/top_level.txt +1 -0
- chatrepl-0.1.0a0/chatrepl.py +423 -0
- chatrepl-0.1.0a0/pyproject.toml +25 -0
- chatrepl-0.1.0a0/setup.cfg +7 -0
chatrepl-0.1.0a0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jifeng Wu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chatrepl
|
|
3
|
+
Version: 0.1.0a0
|
|
4
|
+
Summary: A Python 2.7+ REPL for interacting with LLMs with an OpenAI Chat Completions-compatible API.
|
|
5
|
+
Author-email: Jifeng Wu <jifengwu2k@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jifengwu2k/chatrepl
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/jifengwu2k/chatrepl/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 2
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=2
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
A Python 2.7+ REPL for interacting with LLMs with an OpenAI Chat Completions-compatible API.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Zero Dependencies:** Works with stock Python 2.7+ and 3.x
|
|
22
|
+
- **Interactive Chat:** Natural REPL interface with conversation tracking
|
|
23
|
+
- **Streaming Responses:** See responses as they're generated
|
|
24
|
+
- **Multiline Input:** Support for complex prompts with `:multiline`
|
|
25
|
+
- **File Integration:** Load prompts from text files with `:send <textfile>`
|
|
26
|
+
- **Conversation Persistence:** Save/load complete conversations in JSON format
|
|
27
|
+
- **Enhanced Input:** Optional readline support for history and line editing
|
|
28
|
+
- **Dual Modes:** Both interactive REPL and pipe-friendly CLI
|
|
29
|
+
|
|
30
|
+
## Interactive Mode
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
$ python -m terminal_chat \
|
|
34
|
+
--api-key "your-api-key" \
|
|
35
|
+
--base-url "https://api.openai.com/v1" \
|
|
36
|
+
--model "gpt-4o"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Basic Conversation
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
User [1]: Explain recursion to a 5-year-old
|
|
43
|
+
|
|
44
|
+
Assistant [1]: Imagine you're holding a doll that has...
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Using Files
|
|
48
|
+
```text
|
|
49
|
+
User [2]: :send code.py
|
|
50
|
+
Assistant [2]: I notice this Python code could be improved...
|
|
51
|
+
|
|
52
|
+
User [3]: :save review_chat.json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Multiline Input
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
User [4]: :multiline
|
|
59
|
+
Enter EOF on a blank line to finish input:
|
|
60
|
+
> Compare these programming languages:
|
|
61
|
+
> 1. Python
|
|
62
|
+
> 2. Rust
|
|
63
|
+
> 3. Go
|
|
64
|
+
> [Ctrl-D]
|
|
65
|
+
|
|
66
|
+
Assistant [4]: Here's a comparison:
|
|
67
|
+
1. Python - High-level, interpreted...
|
|
68
|
+
2. Rust - Systems programming...
|
|
69
|
+
3. Go - Compiled, concurrent...
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Non-interactive Mode (Piped Input)
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
$ uname -a | python -m terminal_chat --api-key <your_api_key> --base-url <your_base_url> --model <model_name>
|
|
76
|
+
The output you've provided appears to be system information from ... [output streamed to STDOUT]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Print Saved Conversations
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
$ python -m terminal_chat --print conversation.json
|
|
83
|
+
User [1]: ...
|
|
84
|
+
|
|
85
|
+
Assistant [1]: ...
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Interactive Commands
|
|
89
|
+
|
|
90
|
+
- `:multiline` - Enter multiline input mode (end with blank line + Ctrl-D)
|
|
91
|
+
- `:send TEXTFILE` - Send contents of TEXTFILE
|
|
92
|
+
- `:load JSONFILE` - Load conversation from JSONFILE
|
|
93
|
+
- `:save JSONFILE` - Save conversation to JSONFILE
|
|
94
|
+
- `:help` - Show help
|
|
95
|
+
- `:quit` or `Ctrl-D` - Exit the program
|
|
96
|
+
|
|
97
|
+
## Best Practices
|
|
98
|
+
|
|
99
|
+
1. For long sessions, periodically save with `:save`
|
|
100
|
+
2. Use `:multiline` for structured prompts (lists, code, etc.)
|
|
101
|
+
3. JSON files can be edited manually for prompt engineering
|
|
102
|
+
|
|
103
|
+
## Contributing
|
|
104
|
+
|
|
105
|
+
Contributions are welcome! Please submit pull requests or open issues on the GitHub repository.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
A Python 2.7+ REPL for interacting with LLMs with an OpenAI Chat Completions-compatible API.
|
|
2
|
+
|
|
3
|
+
## Features
|
|
4
|
+
|
|
5
|
+
- **Zero Dependencies:** Works with stock Python 2.7+ and 3.x
|
|
6
|
+
- **Interactive Chat:** Natural REPL interface with conversation tracking
|
|
7
|
+
- **Streaming Responses:** See responses as they're generated
|
|
8
|
+
- **Multiline Input:** Support for complex prompts with `:multiline`
|
|
9
|
+
- **File Integration:** Load prompts from text files with `:send <textfile>`
|
|
10
|
+
- **Conversation Persistence:** Save/load complete conversations in JSON format
|
|
11
|
+
- **Enhanced Input:** Optional readline support for history and line editing
|
|
12
|
+
- **Dual Modes:** Both interactive REPL and pipe-friendly CLI
|
|
13
|
+
|
|
14
|
+
## Interactive Mode
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
$ python -m terminal_chat \
|
|
18
|
+
--api-key "your-api-key" \
|
|
19
|
+
--base-url "https://api.openai.com/v1" \
|
|
20
|
+
--model "gpt-4o"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Basic Conversation
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
User [1]: Explain recursion to a 5-year-old
|
|
27
|
+
|
|
28
|
+
Assistant [1]: Imagine you're holding a doll that has...
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Using Files
|
|
32
|
+
```text
|
|
33
|
+
User [2]: :send code.py
|
|
34
|
+
Assistant [2]: I notice this Python code could be improved...
|
|
35
|
+
|
|
36
|
+
User [3]: :save review_chat.json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Multiline Input
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
User [4]: :multiline
|
|
43
|
+
Enter EOF on a blank line to finish input:
|
|
44
|
+
> Compare these programming languages:
|
|
45
|
+
> 1. Python
|
|
46
|
+
> 2. Rust
|
|
47
|
+
> 3. Go
|
|
48
|
+
> [Ctrl-D]
|
|
49
|
+
|
|
50
|
+
Assistant [4]: Here's a comparison:
|
|
51
|
+
1. Python - High-level, interpreted...
|
|
52
|
+
2. Rust - Systems programming...
|
|
53
|
+
3. Go - Compiled, concurrent...
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Non-interactive Mode (Piped Input)
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
$ uname -a | python -m terminal_chat --api-key <your_api_key> --base-url <your_base_url> --model <model_name>
|
|
60
|
+
The output you've provided appears to be system information from ... [output streamed to STDOUT]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Print Saved Conversations
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
$ python -m terminal_chat --print conversation.json
|
|
67
|
+
User [1]: ...
|
|
68
|
+
|
|
69
|
+
Assistant [1]: ...
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Interactive Commands
|
|
73
|
+
|
|
74
|
+
- `:multiline` - Enter multiline input mode (end with blank line + Ctrl-D)
|
|
75
|
+
- `:send TEXTFILE` - Send contents of TEXTFILE
|
|
76
|
+
- `:load JSONFILE` - Load conversation from JSONFILE
|
|
77
|
+
- `:save JSONFILE` - Save conversation to JSONFILE
|
|
78
|
+
- `:help` - Show help
|
|
79
|
+
- `:quit` or `Ctrl-D` - Exit the program
|
|
80
|
+
|
|
81
|
+
## Best Practices
|
|
82
|
+
|
|
83
|
+
1. For long sessions, periodically save with `:save`
|
|
84
|
+
2. Use `:multiline` for structured prompts (lists, code, etc.)
|
|
85
|
+
3. JSON files can be edited manually for prompt engineering
|
|
86
|
+
|
|
87
|
+
## Contributing
|
|
88
|
+
|
|
89
|
+
Contributions are welcome! Please submit pull requests or open issues on the GitHub repository.
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chatrepl
|
|
3
|
+
Version: 0.1.0a0
|
|
4
|
+
Summary: A Python 2.7+ REPL for interacting with LLMs with an OpenAI Chat Completions-compatible API.
|
|
5
|
+
Author-email: Jifeng Wu <jifengwu2k@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jifengwu2k/chatrepl
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/jifengwu2k/chatrepl/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 2
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=2
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
A Python 2.7+ REPL for interacting with LLMs with an OpenAI Chat Completions-compatible API.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Zero Dependencies:** Works with stock Python 2.7+ and 3.x
|
|
22
|
+
- **Interactive Chat:** Natural REPL interface with conversation tracking
|
|
23
|
+
- **Streaming Responses:** See responses as they're generated
|
|
24
|
+
- **Multiline Input:** Support for complex prompts with `:multiline`
|
|
25
|
+
- **File Integration:** Load prompts from text files with `:send <textfile>`
|
|
26
|
+
- **Conversation Persistence:** Save/load complete conversations in JSON format
|
|
27
|
+
- **Enhanced Input:** Optional readline support for history and line editing
|
|
28
|
+
- **Dual Modes:** Both interactive REPL and pipe-friendly CLI
|
|
29
|
+
|
|
30
|
+
## Interactive Mode
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
$ python -m terminal_chat \
|
|
34
|
+
--api-key "your-api-key" \
|
|
35
|
+
--base-url "https://api.openai.com/v1" \
|
|
36
|
+
--model "gpt-4o"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Basic Conversation
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
User [1]: Explain recursion to a 5-year-old
|
|
43
|
+
|
|
44
|
+
Assistant [1]: Imagine you're holding a doll that has...
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Using Files
|
|
48
|
+
```text
|
|
49
|
+
User [2]: :send code.py
|
|
50
|
+
Assistant [2]: I notice this Python code could be improved...
|
|
51
|
+
|
|
52
|
+
User [3]: :save review_chat.json
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Multiline Input
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
User [4]: :multiline
|
|
59
|
+
Enter EOF on a blank line to finish input:
|
|
60
|
+
> Compare these programming languages:
|
|
61
|
+
> 1. Python
|
|
62
|
+
> 2. Rust
|
|
63
|
+
> 3. Go
|
|
64
|
+
> [Ctrl-D]
|
|
65
|
+
|
|
66
|
+
Assistant [4]: Here's a comparison:
|
|
67
|
+
1. Python - High-level, interpreted...
|
|
68
|
+
2. Rust - Systems programming...
|
|
69
|
+
3. Go - Compiled, concurrent...
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Non-interactive Mode (Piped Input)
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
$ uname -a | python -m terminal_chat --api-key <your_api_key> --base-url <your_base_url> --model <model_name>
|
|
76
|
+
The output you've provided appears to be system information from ... [output streamed to STDOUT]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Print Saved Conversations
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
$ python -m terminal_chat --print conversation.json
|
|
83
|
+
User [1]: ...
|
|
84
|
+
|
|
85
|
+
Assistant [1]: ...
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Interactive Commands
|
|
89
|
+
|
|
90
|
+
- `:multiline` - Enter multiline input mode (end with blank line + Ctrl-D)
|
|
91
|
+
- `:send TEXTFILE` - Send contents of TEXTFILE
|
|
92
|
+
- `:load JSONFILE` - Load conversation from JSONFILE
|
|
93
|
+
- `:save JSONFILE` - Save conversation to JSONFILE
|
|
94
|
+
- `:help` - Show help
|
|
95
|
+
- `:quit` or `Ctrl-D` - Exit the program
|
|
96
|
+
|
|
97
|
+
## Best Practices
|
|
98
|
+
|
|
99
|
+
1. For long sessions, periodically save with `:save`
|
|
100
|
+
2. Use `:multiline` for structured prompts (lists, code, etc.)
|
|
101
|
+
3. JSON files can be edited manually for prompt engineering
|
|
102
|
+
|
|
103
|
+
## Contributing
|
|
104
|
+
|
|
105
|
+
Contributions are welcome! Please submit pull requests or open issues on the GitHub repository.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
chatrepl
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
from __future__ import print_function
|
|
4
|
+
import argparse
|
|
5
|
+
import codecs
|
|
6
|
+
import getopt
|
|
7
|
+
import io
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import os.path
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
# Python version compatibility setup
|
|
14
|
+
if sys.version_info < (3,):
|
|
15
|
+
from urllib2 import Request, urlopen, URLError
|
|
16
|
+
text_type = unicode
|
|
17
|
+
binary_type = str
|
|
18
|
+
|
|
19
|
+
def unicode_input(prompt):
|
|
20
|
+
return raw_input(prompt).decode(sys.stdin.encoding)
|
|
21
|
+
|
|
22
|
+
open_with_encoding = codecs.open
|
|
23
|
+
|
|
24
|
+
def post_request_instance(url, data, headers):
|
|
25
|
+
return Request(url, data=data, headers=headers)
|
|
26
|
+
else:
|
|
27
|
+
from urllib.request import Request, urlopen
|
|
28
|
+
from urllib.error import URLError
|
|
29
|
+
text_type = str
|
|
30
|
+
binary_type = bytes
|
|
31
|
+
unicode_input = input
|
|
32
|
+
open_with_encoding = open
|
|
33
|
+
|
|
34
|
+
def post_request_instance(url, data, headers):
|
|
35
|
+
return Request(url, data=data, headers=headers, method='POST')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Are we under interactive mode?
|
|
39
|
+
IS_INTERACTIVE = sys.stdin.isatty()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def sanitize(text):
|
|
43
|
+
return text.encode(sys.stdout.encoding, errors='ignore').decode(sys.stdout.encoding)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def fputs(text, stream):
|
|
47
|
+
"""Write text to a stream."""
|
|
48
|
+
try:
|
|
49
|
+
stream.write(text)
|
|
50
|
+
stream.flush()
|
|
51
|
+
except UnicodeEncodeError:
|
|
52
|
+
fputs(sanitize(text), stream)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def perror(e):
|
|
56
|
+
fputs(u'%s: %s\n' % (type(e).__name__, e), sys.stderr)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Try to import readline under interactive mode
|
|
60
|
+
if IS_INTERACTIVE:
|
|
61
|
+
try:
|
|
62
|
+
import readline
|
|
63
|
+
except ImportError as e:
|
|
64
|
+
readline = None
|
|
65
|
+
perror(e)
|
|
66
|
+
fputs(u"\nFailed to import readline. This will affect the command-line interface functionality:\n", sys.stderr)
|
|
67
|
+
fputs(u" - Line editing features (arrow keys, cursor movement) will be disabled\n", sys.stderr)
|
|
68
|
+
fputs(u" - Command history (up/down keys) will not be available\n", sys.stderr)
|
|
69
|
+
fputs(u"\nWhile the program will still run, the text input will be basic and limited.\n", sys.stderr)
|
|
70
|
+
else:
|
|
71
|
+
readline = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def save_messages_to_file(messages, filename):
|
|
75
|
+
"""Save chat messages to a JSON file."""
|
|
76
|
+
with open_with_encoding(filename, 'w', encoding='utf-8') as f:
|
|
77
|
+
json.dump(messages, f, indent=2, ensure_ascii=False)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def load_messages_from_file(filename):
|
|
81
|
+
"""Load chat messages from a JSON file."""
|
|
82
|
+
with open_with_encoding(filename, 'r', encoding='utf-8') as f:
|
|
83
|
+
loaded = json.load(f)
|
|
84
|
+
if isinstance(loaded, list) and all(
|
|
85
|
+
(
|
|
86
|
+
isinstance(message, dict)
|
|
87
|
+
and message.get(u'role', None) in (u'user', u'assistant')
|
|
88
|
+
and isinstance(message.get(u'content', None), text_type)
|
|
89
|
+
)
|
|
90
|
+
for message in loaded
|
|
91
|
+
):
|
|
92
|
+
return loaded
|
|
93
|
+
else:
|
|
94
|
+
raise ValueError(u"Invalid JSON schema: Expected a list of dictionaries with keys 'role' ('user' or 'assistant') and 'content' (string). Got: %s" % loaded)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def print_loaded_messages(loaded_message_list):
|
|
98
|
+
"""Print all loaded messages.
|
|
99
|
+
Return number of user messages."""
|
|
100
|
+
num_user_messages_encountered = 0
|
|
101
|
+
|
|
102
|
+
for message in loaded_message_list:
|
|
103
|
+
role = message[u'role']
|
|
104
|
+
content = message[u'content']
|
|
105
|
+
|
|
106
|
+
if role == 'user':
|
|
107
|
+
num_user_messages_encountered += 1
|
|
108
|
+
|
|
109
|
+
fputs(
|
|
110
|
+
u'\n%s [%i]: %s\n' % (
|
|
111
|
+
role.capitalize(),
|
|
112
|
+
num_user_messages_encountered,
|
|
113
|
+
content
|
|
114
|
+
),
|
|
115
|
+
sys.stdout
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
return num_user_messages_encountered
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def read_file_content(filename):
|
|
122
|
+
"""Read text_type content from a file."""
|
|
123
|
+
with open_with_encoding(filename, 'r', encoding='utf-8') as f:
|
|
124
|
+
return f.read()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# Global message counter
|
|
128
|
+
# Like IPython's message counter, incremented for non-empty user messages
|
|
129
|
+
# Read in `get_single_message_content_from_model`
|
|
130
|
+
# Written in `get_single_message_content_from_user`, `main`
|
|
131
|
+
global_message_counter = 1
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# Model-facing functions
|
|
135
|
+
def send_messages_to_model_and_stream_response(
|
|
136
|
+
api_key,
|
|
137
|
+
base_url,
|
|
138
|
+
model,
|
|
139
|
+
message_list
|
|
140
|
+
):
|
|
141
|
+
"""Send messages to the model using standard library HTTP requests."""
|
|
142
|
+
url = u"%s/chat/completions" % base_url
|
|
143
|
+
headers = {
|
|
144
|
+
u"Content-Type": u"application/json",
|
|
145
|
+
u"Authorization": u"Bearer %s" % api_key
|
|
146
|
+
}
|
|
147
|
+
data = json.dumps({
|
|
148
|
+
u"model": model,
|
|
149
|
+
u"messages": message_list,
|
|
150
|
+
u"stream": True
|
|
151
|
+
}).encode('utf-8')
|
|
152
|
+
|
|
153
|
+
req = post_request_instance(url, data=data, headers=headers)
|
|
154
|
+
response = urlopen(req)
|
|
155
|
+
for line in response:
|
|
156
|
+
if line.startswith(b"data: "):
|
|
157
|
+
chunk_data = line[6:].decode('utf-8').strip() # Remove "data: " prefix
|
|
158
|
+
if chunk_data == u"[DONE]":
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
chunk = json.loads(chunk_data)
|
|
162
|
+
content = chunk.get(u"choices", [{}])[0].get(u"delta", {}).get(u"content", "") or u""
|
|
163
|
+
yield content
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_single_message_content_from_model(
|
|
167
|
+
api_key,
|
|
168
|
+
base_url,
|
|
169
|
+
model,
|
|
170
|
+
message_list
|
|
171
|
+
):
|
|
172
|
+
global global_message_counter
|
|
173
|
+
|
|
174
|
+
fputs(
|
|
175
|
+
u'\nAssistant [%d]: ' % global_message_counter,
|
|
176
|
+
sys.stdout
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
full_response = []
|
|
180
|
+
for response in send_messages_to_model_and_stream_response(
|
|
181
|
+
api_key,
|
|
182
|
+
base_url,
|
|
183
|
+
model,
|
|
184
|
+
message_list
|
|
185
|
+
):
|
|
186
|
+
fputs(response, sys.stdout)
|
|
187
|
+
full_response.append(response)
|
|
188
|
+
|
|
189
|
+
fputs(u'\n', sys.stdout)
|
|
190
|
+
|
|
191
|
+
return u''.join(full_response)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# User-facing functions
|
|
195
|
+
def display_help():
|
|
196
|
+
fputs(u'\nEnter a message to send to the model or use one of the following commands:\n', sys.stdout)
|
|
197
|
+
fputs(u':multiline Enter multiline input\n', sys.stdout)
|
|
198
|
+
fputs(u':send TEXTFILE Send the contents of TEXTFILE\n', sys.stdout)
|
|
199
|
+
fputs(u':load JSONFILE Load a conversation from JSONFILE\n', sys.stdout)
|
|
200
|
+
fputs(u':save JSONFILE Save the conversation to JSONFILE\n', sys.stdout)
|
|
201
|
+
fputs(u':help Display help\n', sys.stdout)
|
|
202
|
+
fputs(u':quit (or Ctrl-D) Exit the program\n', sys.stdout)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def get_single_line_input(prompt=u'> '):
|
|
206
|
+
"""Get a single-line input from the user."""
|
|
207
|
+
# Do NOT use unicode_input().
|
|
208
|
+
# When the user enters something and presses down BACKSPACE, the prompt is removed as well.
|
|
209
|
+
return unicode_input(prompt)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def get_multi_line_input():
|
|
213
|
+
"""Get multi-line input from the user until EOF."""
|
|
214
|
+
fputs(u'Enter EOF on a blank line to finish input:\n', sys.stdout)
|
|
215
|
+
lines = []
|
|
216
|
+
try:
|
|
217
|
+
while True:
|
|
218
|
+
line = get_single_line_input(u'> ')
|
|
219
|
+
lines.append(line)
|
|
220
|
+
except EOFError as e:
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
return u'\n'.join(lines)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def get_single_message_content_from_user(mutable_message_list):
|
|
227
|
+
global global_message_counter
|
|
228
|
+
|
|
229
|
+
while True:
|
|
230
|
+
user_input = get_single_line_input(
|
|
231
|
+
u'\nUser [%d]: ' % global_message_counter
|
|
232
|
+
).strip()
|
|
233
|
+
|
|
234
|
+
if user_input.startswith(u':'):
|
|
235
|
+
tokens = user_input.split()
|
|
236
|
+
cmd = tokens[0] if tokens else u""
|
|
237
|
+
args = tokens[1:]
|
|
238
|
+
|
|
239
|
+
# :multiline
|
|
240
|
+
if cmd == u":multiline" and not args:
|
|
241
|
+
content = get_multi_line_input()
|
|
242
|
+
# :send <textfile>
|
|
243
|
+
elif cmd == u":send" and len(args) == 1:
|
|
244
|
+
try:
|
|
245
|
+
content = read_file_content(args[0])
|
|
246
|
+
except Exception as e:
|
|
247
|
+
perror(e)
|
|
248
|
+
|
|
249
|
+
global_message_counter += 1
|
|
250
|
+
continue
|
|
251
|
+
# :load <jsonfile>
|
|
252
|
+
elif cmd == u":load" and len(args) == 1:
|
|
253
|
+
try:
|
|
254
|
+
loaded_messages = load_messages_from_file(args[0])
|
|
255
|
+
fputs(u"Loaded conversation from %s:\n" % args[0], sys.stdout)
|
|
256
|
+
|
|
257
|
+
last_message_counter = print_loaded_messages(loaded_messages)
|
|
258
|
+
|
|
259
|
+
mutable_message_list[:] = loaded_messages
|
|
260
|
+
global_message_counter = last_message_counter + 1
|
|
261
|
+
continue
|
|
262
|
+
except Exception as e:
|
|
263
|
+
perror(e)
|
|
264
|
+
|
|
265
|
+
global_message_counter += 1
|
|
266
|
+
continue
|
|
267
|
+
# :save <jsonfile>
|
|
268
|
+
elif cmd == u":save" and len(args) == 1:
|
|
269
|
+
try:
|
|
270
|
+
save_messages_to_file(mutable_message_list, args[0])
|
|
271
|
+
fputs(u"Conversation saved to %s\n" % args[0], sys.stdout)
|
|
272
|
+
except Exception as e:
|
|
273
|
+
perror(e)
|
|
274
|
+
|
|
275
|
+
global_message_counter += 1
|
|
276
|
+
continue
|
|
277
|
+
# :help
|
|
278
|
+
elif cmd == u":help" and not args:
|
|
279
|
+
display_help()
|
|
280
|
+
|
|
281
|
+
global_message_counter += 1
|
|
282
|
+
continue
|
|
283
|
+
# :quit
|
|
284
|
+
elif cmd == u":quit" and not args:
|
|
285
|
+
# Same as pressing Ctrl-D (sending EOF)
|
|
286
|
+
raise EOFError
|
|
287
|
+
else:
|
|
288
|
+
fputs(u"Unknown command.\n", sys.stdout)
|
|
289
|
+
display_help()
|
|
290
|
+
|
|
291
|
+
global_message_counter += 1
|
|
292
|
+
continue
|
|
293
|
+
else:
|
|
294
|
+
content = user_input
|
|
295
|
+
|
|
296
|
+
# We do not allow content to be empty
|
|
297
|
+
if not content:
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
return content
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def main():
|
|
304
|
+
"""Entry point for the chat interface."""
|
|
305
|
+
global global_message_counter
|
|
306
|
+
|
|
307
|
+
# Create the parser
|
|
308
|
+
parser = argparse.ArgumentParser()
|
|
309
|
+
|
|
310
|
+
# Add arguments
|
|
311
|
+
parser.add_argument('--api-key', type=str, required=False, help='API key')
|
|
312
|
+
parser.add_argument('--base-url', type=str, required=False, help='Base URL')
|
|
313
|
+
parser.add_argument('--model', type=str, required=False, help='Model name')
|
|
314
|
+
parser.add_argument('--load', metavar='JSONFILE', type=str, required=False, help='Load a conversation from JSONFILE')
|
|
315
|
+
parser.add_argument('--print', metavar='JSONFILE', type=str, required=False, help='Print a saved conversation from JSONFILE and exit')
|
|
316
|
+
|
|
317
|
+
# Parse arguments
|
|
318
|
+
args = parser.parse_args()
|
|
319
|
+
|
|
320
|
+
# Print a saved conversation from JSONFILE and exit
|
|
321
|
+
if args.print:
|
|
322
|
+
print_loaded_messages(load_messages_from_file(args.print))
|
|
323
|
+
else:
|
|
324
|
+
if (
|
|
325
|
+
not args.api_key
|
|
326
|
+
or not args.base_url
|
|
327
|
+
or not args.model
|
|
328
|
+
):
|
|
329
|
+
parser.error('Must provide --api-key, --base-url, and --model when not printing a conversation using --print')
|
|
330
|
+
|
|
331
|
+
# Initialize messages
|
|
332
|
+
if args.load:
|
|
333
|
+
messages = load_messages_from_file(args.load)
|
|
334
|
+
else:
|
|
335
|
+
messages = []
|
|
336
|
+
|
|
337
|
+
# Non-interactive mode
|
|
338
|
+
if not IS_INTERACTIVE:
|
|
339
|
+
user_message_content = sys.stdin.read()
|
|
340
|
+
|
|
341
|
+
if user_message_content:
|
|
342
|
+
messages.append(
|
|
343
|
+
{
|
|
344
|
+
u'role': 'user',
|
|
345
|
+
u'content': user_message_content
|
|
346
|
+
}
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
for response in send_messages_to_model_and_stream_response(
|
|
350
|
+
args.api_key,
|
|
351
|
+
args.base_url,
|
|
352
|
+
args.model,
|
|
353
|
+
messages
|
|
354
|
+
):
|
|
355
|
+
fputs(response, sys.stdout)
|
|
356
|
+
|
|
357
|
+
fputs(u'\n', sys.stdout)
|
|
358
|
+
|
|
359
|
+
# Interactive mode
|
|
360
|
+
else:
|
|
361
|
+
# Read readline history file
|
|
362
|
+
if readline is not None:
|
|
363
|
+
histfile = os.path.join(os.path.expanduser("~"), ".chat_history")
|
|
364
|
+
try:
|
|
365
|
+
readline.read_history_file(histfile)
|
|
366
|
+
except Exception:
|
|
367
|
+
pass
|
|
368
|
+
|
|
369
|
+
# Display greeting
|
|
370
|
+
fputs(
|
|
371
|
+
u'\nWelcome to Terminal Chat (Model: %s)\n' % args.model,
|
|
372
|
+
sys.stdout
|
|
373
|
+
)
|
|
374
|
+
display_help()
|
|
375
|
+
|
|
376
|
+
# Main loop
|
|
377
|
+
while True:
|
|
378
|
+
try:
|
|
379
|
+
user_message_content = get_single_message_content_from_user(
|
|
380
|
+
messages
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
messages.append(
|
|
384
|
+
{
|
|
385
|
+
u'role': 'user',
|
|
386
|
+
u'content': user_message_content
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
except EOFError:
|
|
390
|
+
break
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
assistant_message_content = get_single_message_content_from_model(
|
|
394
|
+
args.api_key,
|
|
395
|
+
args.base_url,
|
|
396
|
+
args.model,
|
|
397
|
+
messages
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
messages.append(
|
|
401
|
+
{
|
|
402
|
+
u'role': u'assistant',
|
|
403
|
+
u'content': assistant_message_content
|
|
404
|
+
}
|
|
405
|
+
)
|
|
406
|
+
except URLError as e:
|
|
407
|
+
perror(e)
|
|
408
|
+
|
|
409
|
+
# Also remove the last user message
|
|
410
|
+
messages.pop()
|
|
411
|
+
|
|
412
|
+
# And skip incrementing global_message_counter
|
|
413
|
+
continue
|
|
414
|
+
|
|
415
|
+
global_message_counter += 1
|
|
416
|
+
|
|
417
|
+
# Write readline history file before exiting
|
|
418
|
+
if readline is not None:
|
|
419
|
+
readline.write_history_file(histfile)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
if __name__ == "__main__":
|
|
423
|
+
main()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "chatrepl"
|
|
7
|
+
version = "0.1.0-alpha"
|
|
8
|
+
description = "A Python 2.7+ REPL for interacting with LLMs with an OpenAI Chat Completions-compatible API."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=2"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name="Jifeng Wu", email="jifengwu2k@gmail.com" }
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 2",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
"Homepage" = "https://github.com/jifengwu2k/chatrepl"
|
|
25
|
+
"Bug Tracker" = "https://github.com/jifengwu2k/chatrepl/issues"
|