askllm-cli 0.1.0__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.
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.4
2
+ Name: askllm-cli
3
+ Version: 0.1.0
4
+ Summary: Lightweight OpenAI-compatible terminal REPL
5
+ Author: AskLLM
6
+ License: MIT
7
+ Keywords: llm,repl,cli,openai,terminal,ai
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Communications :: Chat
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.0; extra == "dev"
25
+
26
+ # AskLLM
27
+
28
+ A lightweight, zero-dependency Python REPL for chatting with LLMs via any OpenAI-compatible API.
29
+
30
+ Designed to be installed via `pip install askllm-cli` (or `pipx install askllm-cli`), and developed using **Podman** with `python:3.12-slim` (or run directly with Python 3).
31
+
32
+ ---
33
+
34
+ ## Features
35
+
36
+ - **Zero Third-Party Runtime Dependencies:** Uses standard library only (`urllib.request`, `json`, `readline`).
37
+ - **Standard Python Package:** Modern PEP 517/621 packaging (`pyproject.toml`) published as `askllm-cli` on PyPI.
38
+ - **Podman Development Ready:** Built-in development workflow using `python:3.12-slim` (`./dev.sh`).
39
+ - **Environment Variable Auto-Detection:** Automatically picks up standard OpenAI / Azure / local LLM variables.
40
+ - **Streaming Responses:** Real-time token streaming via Server-Sent Events (SSE).
41
+ - **Graceful Signal Handling:**
42
+ - `Ctrl+C` or `Ctrl+D` at the beginning of the line exits the REPL.
43
+ - `Ctrl+C` with text in the buffer cancels the line.
44
+ - `Ctrl+C` during response streaming halts generation cleanly without exiting.
45
+ - **Multi-Turn Chat History:** Maintains conversation context across turns within the session.
46
+ - **Persistent Input History:** Readline history saved across runs in `~/.askllm_history`.
47
+ - **Multiline Input Support:**
48
+ - Triple quotes `"""` ... `"""`
49
+ - Trailing backslash `\`
50
+ - `/paste` command
51
+
52
+ ---
53
+
54
+ ## Development with Podman (`python:3.12-slim`)
55
+
56
+ No local Python installation or host dependencies are required. A complete development environment is provided via Podman:
57
+
58
+ ### 1. Interactive Development Shell
59
+ Drop into an interactive bash shell running inside `python:3.12-slim` with AskLLM installed in editable mode (`pip install -e .`):
60
+
61
+ ```bash
62
+ ./dev.sh
63
+ ```
64
+
65
+ Inside the shell:
66
+ ```bash
67
+ # Run AskLLM directly
68
+ askllm --help
69
+
70
+ # Run tests
71
+ pytest
72
+
73
+ # Test interactive python
74
+ python3 -c "import askllm; print(askllm.__version__)"
75
+ ```
76
+
77
+ Any changes made to files in `src/askllm` on your host are immediately reflected inside the container!
78
+
79
+ ### 2. Run Tests in Podman
80
+ Run the test suite inside the `python:3.12-slim` container:
81
+
82
+ ```bash
83
+ ./dev.sh test
84
+ ```
85
+
86
+ ### 3. Run AskLLM via Podman
87
+ Run AskLLM using Podman directly from the host:
88
+
89
+ ```bash
90
+ ./askllm
91
+ ```
92
+ or with arguments:
93
+ ```bash
94
+ ./askllm --model llama3.2 --endpoint http://localhost:11434/v1
95
+ ```
96
+
97
+ ### 4. Dev Container (VS Code / IDEs)
98
+ Open this repository in VS Code or any editor supporting Dev Containers to develop seamlessly inside the `python:3.12-slim` container.
99
+
100
+ ---
101
+
102
+ ## Installation via `pip` / `pipx`
103
+
104
+ Install AskLLM from PyPI:
105
+
106
+ ```bash
107
+ # Recommended for CLI tools
108
+ pipx install askllm-cli
109
+
110
+ # Or via standard pip
111
+ pip install askllm-cli
112
+ ```
113
+
114
+ Or install locally in editable mode in any Python virtual environment:
115
+
116
+ ```bash
117
+ # Editable install
118
+ pip install -e .
119
+
120
+ # With dev dependencies (pytest)
121
+ pip install -e ".[dev]"
122
+ ```
123
+
124
+ Once installed, the `askllm` command is directly available:
125
+
126
+ ```bash
127
+ askllm --help
128
+ ```
129
+
130
+ Or run as a module:
131
+ ```bash
132
+ python3 -m askllm
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Production Container
138
+
139
+ Build and run the production image using Podman:
140
+
141
+ ```bash
142
+ podman build -t askllm .
143
+ podman run --rm -it --network=host -e OPENAI_API_KEY askllm
144
+ ```
145
+
146
+ ---
147
+
148
+ ## Environment Variables
149
+
150
+ AskLLM automatically checks for these environment variables:
151
+
152
+ | Variable | Description | Default |
153
+ | :--- | :--- | :--- |
154
+ | `OPENAI_API_KEY` | Your API key | *(empty / none)* |
155
+ | `OPENAI_ENDPOINT` or `OPENAI_BASE_URL` or `OPENAI_API_BASE` | API base URL or chat endpoint | `https://api.openai.com/v1` |
156
+ | `OPENAI_MODEL` | Default model name | `gpt-4o-mini` |
157
+ | `OPENAI_SYSTEM_PROMPT` | Custom system prompt | `"You are a helpful assistant."` |
158
+
159
+ ---
160
+
161
+ ## Using with Local LLMs (Ollama, LM Studio, vLLM, etc.)
162
+
163
+ Because `./askllm` and `./dev.sh` use host networking (`--network=host`), you can connect directly to local servers:
164
+
165
+ #### Ollama:
166
+ ```bash
167
+ OPENAI_ENDPOINT="http://localhost:11434/v1" OPENAI_MODEL="llama3.2" ./askllm
168
+ ```
169
+
170
+ #### LM Studio / LocalAI / vLLM:
171
+ ```bash
172
+ OPENAI_ENDPOINT="http://localhost:1234/v1" OPENAI_MODEL="local-model" ./askllm
173
+ ```
174
+
175
+ ---
176
+
177
+ ## REPL Slash Commands
178
+
179
+ Inside the REPL, type `/` to access built-in commands:
180
+
181
+ | Command | Action |
182
+ | :--- | :--- |
183
+ | `/help` | Display command help and tips |
184
+ | `/clear` or `/reset` | Clear session conversation history |
185
+ | `/model [name]` | Show or dynamically switch model |
186
+ | `/endpoint` | Show currently configured endpoint URL |
187
+ | `/system [prompt]` | Show or update the system prompt |
188
+ | `/history` | Show full message history for the session |
189
+ | `/paste` | Enter multiline paste mode |
190
+ | `/exit` or `/quit` | Exit the REPL |
191
+
192
+ ---
193
+
194
+ ## Command-Line Options
195
+
196
+ ```
197
+ usage: askllm [-h] [-v] [-m MODEL] [-e ENDPOINT] [-k API_KEY] [-s SYSTEM]
198
+
199
+ options:
200
+ -h, --help show this help message and exit
201
+ -v, --version show program's version number and exit
202
+ -m MODEL, --model MODEL
203
+ LLM model name (env: OPENAI_MODEL, default: gpt-4o-mini)
204
+ -e ENDPOINT, --endpoint ENDPOINT
205
+ API Base URL / Endpoint (env: OPENAI_BASE_URL, OPENAI_ENDPOINT, OPENAI_API_BASE)
206
+ -k API_KEY, --api-key API_KEY
207
+ API Key (env: OPENAI_API_KEY)
208
+ -s SYSTEM, --system SYSTEM
209
+ System prompt (env: OPENAI_SYSTEM_PROMPT)
210
+ ```
@@ -0,0 +1,185 @@
1
+ # AskLLM
2
+
3
+ A lightweight, zero-dependency Python REPL for chatting with LLMs via any OpenAI-compatible API.
4
+
5
+ Designed to be installed via `pip install askllm-cli` (or `pipx install askllm-cli`), and developed using **Podman** with `python:3.12-slim` (or run directly with Python 3).
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - **Zero Third-Party Runtime Dependencies:** Uses standard library only (`urllib.request`, `json`, `readline`).
12
+ - **Standard Python Package:** Modern PEP 517/621 packaging (`pyproject.toml`) published as `askllm-cli` on PyPI.
13
+ - **Podman Development Ready:** Built-in development workflow using `python:3.12-slim` (`./dev.sh`).
14
+ - **Environment Variable Auto-Detection:** Automatically picks up standard OpenAI / Azure / local LLM variables.
15
+ - **Streaming Responses:** Real-time token streaming via Server-Sent Events (SSE).
16
+ - **Graceful Signal Handling:**
17
+ - `Ctrl+C` or `Ctrl+D` at the beginning of the line exits the REPL.
18
+ - `Ctrl+C` with text in the buffer cancels the line.
19
+ - `Ctrl+C` during response streaming halts generation cleanly without exiting.
20
+ - **Multi-Turn Chat History:** Maintains conversation context across turns within the session.
21
+ - **Persistent Input History:** Readline history saved across runs in `~/.askllm_history`.
22
+ - **Multiline Input Support:**
23
+ - Triple quotes `"""` ... `"""`
24
+ - Trailing backslash `\`
25
+ - `/paste` command
26
+
27
+ ---
28
+
29
+ ## Development with Podman (`python:3.12-slim`)
30
+
31
+ No local Python installation or host dependencies are required. A complete development environment is provided via Podman:
32
+
33
+ ### 1. Interactive Development Shell
34
+ Drop into an interactive bash shell running inside `python:3.12-slim` with AskLLM installed in editable mode (`pip install -e .`):
35
+
36
+ ```bash
37
+ ./dev.sh
38
+ ```
39
+
40
+ Inside the shell:
41
+ ```bash
42
+ # Run AskLLM directly
43
+ askllm --help
44
+
45
+ # Run tests
46
+ pytest
47
+
48
+ # Test interactive python
49
+ python3 -c "import askllm; print(askllm.__version__)"
50
+ ```
51
+
52
+ Any changes made to files in `src/askllm` on your host are immediately reflected inside the container!
53
+
54
+ ### 2. Run Tests in Podman
55
+ Run the test suite inside the `python:3.12-slim` container:
56
+
57
+ ```bash
58
+ ./dev.sh test
59
+ ```
60
+
61
+ ### 3. Run AskLLM via Podman
62
+ Run AskLLM using Podman directly from the host:
63
+
64
+ ```bash
65
+ ./askllm
66
+ ```
67
+ or with arguments:
68
+ ```bash
69
+ ./askllm --model llama3.2 --endpoint http://localhost:11434/v1
70
+ ```
71
+
72
+ ### 4. Dev Container (VS Code / IDEs)
73
+ Open this repository in VS Code or any editor supporting Dev Containers to develop seamlessly inside the `python:3.12-slim` container.
74
+
75
+ ---
76
+
77
+ ## Installation via `pip` / `pipx`
78
+
79
+ Install AskLLM from PyPI:
80
+
81
+ ```bash
82
+ # Recommended for CLI tools
83
+ pipx install askllm-cli
84
+
85
+ # Or via standard pip
86
+ pip install askllm-cli
87
+ ```
88
+
89
+ Or install locally in editable mode in any Python virtual environment:
90
+
91
+ ```bash
92
+ # Editable install
93
+ pip install -e .
94
+
95
+ # With dev dependencies (pytest)
96
+ pip install -e ".[dev]"
97
+ ```
98
+
99
+ Once installed, the `askllm` command is directly available:
100
+
101
+ ```bash
102
+ askllm --help
103
+ ```
104
+
105
+ Or run as a module:
106
+ ```bash
107
+ python3 -m askllm
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Production Container
113
+
114
+ Build and run the production image using Podman:
115
+
116
+ ```bash
117
+ podman build -t askllm .
118
+ podman run --rm -it --network=host -e OPENAI_API_KEY askllm
119
+ ```
120
+
121
+ ---
122
+
123
+ ## Environment Variables
124
+
125
+ AskLLM automatically checks for these environment variables:
126
+
127
+ | Variable | Description | Default |
128
+ | :--- | :--- | :--- |
129
+ | `OPENAI_API_KEY` | Your API key | *(empty / none)* |
130
+ | `OPENAI_ENDPOINT` or `OPENAI_BASE_URL` or `OPENAI_API_BASE` | API base URL or chat endpoint | `https://api.openai.com/v1` |
131
+ | `OPENAI_MODEL` | Default model name | `gpt-4o-mini` |
132
+ | `OPENAI_SYSTEM_PROMPT` | Custom system prompt | `"You are a helpful assistant."` |
133
+
134
+ ---
135
+
136
+ ## Using with Local LLMs (Ollama, LM Studio, vLLM, etc.)
137
+
138
+ Because `./askllm` and `./dev.sh` use host networking (`--network=host`), you can connect directly to local servers:
139
+
140
+ #### Ollama:
141
+ ```bash
142
+ OPENAI_ENDPOINT="http://localhost:11434/v1" OPENAI_MODEL="llama3.2" ./askllm
143
+ ```
144
+
145
+ #### LM Studio / LocalAI / vLLM:
146
+ ```bash
147
+ OPENAI_ENDPOINT="http://localhost:1234/v1" OPENAI_MODEL="local-model" ./askllm
148
+ ```
149
+
150
+ ---
151
+
152
+ ## REPL Slash Commands
153
+
154
+ Inside the REPL, type `/` to access built-in commands:
155
+
156
+ | Command | Action |
157
+ | :--- | :--- |
158
+ | `/help` | Display command help and tips |
159
+ | `/clear` or `/reset` | Clear session conversation history |
160
+ | `/model [name]` | Show or dynamically switch model |
161
+ | `/endpoint` | Show currently configured endpoint URL |
162
+ | `/system [prompt]` | Show or update the system prompt |
163
+ | `/history` | Show full message history for the session |
164
+ | `/paste` | Enter multiline paste mode |
165
+ | `/exit` or `/quit` | Exit the REPL |
166
+
167
+ ---
168
+
169
+ ## Command-Line Options
170
+
171
+ ```
172
+ usage: askllm [-h] [-v] [-m MODEL] [-e ENDPOINT] [-k API_KEY] [-s SYSTEM]
173
+
174
+ options:
175
+ -h, --help show this help message and exit
176
+ -v, --version show program's version number and exit
177
+ -m MODEL, --model MODEL
178
+ LLM model name (env: OPENAI_MODEL, default: gpt-4o-mini)
179
+ -e ENDPOINT, --endpoint ENDPOINT
180
+ API Base URL / Endpoint (env: OPENAI_BASE_URL, OPENAI_ENDPOINT, OPENAI_API_BASE)
181
+ -k API_KEY, --api-key API_KEY
182
+ API Key (env: OPENAI_API_KEY)
183
+ -s SYSTEM, --system SYSTEM
184
+ System prompt (env: OPENAI_SYSTEM_PROMPT)
185
+ ```
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "askllm-cli"
7
+ version = "0.1.0"
8
+ description = "Lightweight OpenAI-compatible terminal REPL"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "AskLLM" }
14
+ ]
15
+ keywords = ["llm", "repl", "cli", "openai", "terminal", "ai"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.8",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Topic :: Communications :: Chat",
29
+ "Topic :: Utilities",
30
+ ]
31
+ dependencies = []
32
+
33
+ [project.optional-dependencies]
34
+ dev = [
35
+ "pytest>=7.0",
36
+ ]
37
+
38
+ [project.scripts]
39
+ askllm = "askllm.cli:main"
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ """
2
+ AskLLM - A lightweight Python REPL for asking LLMs (OpenAI-compatible endpoints).
3
+ Zero external dependencies (uses Python standard library).
4
+ """
5
+
6
+ __version__ = "0.1.0"
7
+
8
+ from askllm.cli import LLMClient, REPL, main
9
+
10
+ __all__ = ["LLMClient", "REPL", "main", "__version__"]
@@ -0,0 +1,6 @@
1
+ """Entry point for python -m askllm."""
2
+
3
+ from askllm.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,431 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ askllm - A lightweight Python REPL for asking LLMs (OpenAI-compatible endpoints).
4
+ Zero external dependencies (uses Python standard library).
5
+ """
6
+
7
+ import atexit
8
+ import json
9
+ import os
10
+ import readline
11
+ import signal
12
+ import sys
13
+ import urllib.error
14
+ import urllib.parse
15
+ import urllib.request
16
+ from typing import List, Dict, Generator, Optional, Tuple
17
+
18
+ try:
19
+ from askllm import __version__
20
+ except ImportError:
21
+ __version__ = "0.1.0"
22
+
23
+ # ANSI color codes
24
+ COLOR_RESET = "\033[0m"
25
+ COLOR_BOLD = "\033[1m"
26
+ COLOR_DIM = "\033[2m"
27
+ COLOR_CYAN = "\033[36m"
28
+ COLOR_GREEN = "\033[32m"
29
+ COLOR_YELLOW = "\033[33m"
30
+ COLOR_RED = "\033[31m"
31
+ COLOR_MAGENTA = "\033[35m"
32
+
33
+ # Disable colors if not a TTY or NO_COLOR is set
34
+ if not sys.stdout.isatty() or os.environ.get("NO_COLOR"):
35
+ COLOR_RESET = ""
36
+ COLOR_BOLD = ""
37
+ COLOR_DIM = ""
38
+ COLOR_CYAN = ""
39
+ COLOR_GREEN = ""
40
+ COLOR_YELLOW = ""
41
+ COLOR_RED = ""
42
+ COLOR_MAGENTA = ""
43
+
44
+
45
+ def get_default_env(key_names: List[str], default: Optional[str] = None) -> Optional[str]:
46
+ """Return the first set environment variable from a list of aliases."""
47
+ for key in key_names:
48
+ val = os.environ.get(key)
49
+ if val and val.strip():
50
+ return val.strip()
51
+ return default
52
+
53
+
54
+ def resolve_chat_url(base_url: str) -> str:
55
+ """Normalize user-provided base URL or endpoint to chat completions URL."""
56
+ if not base_url:
57
+ return "https://api.openai.com/v1/chat/completions"
58
+
59
+ parsed = urllib.parse.urlparse(base_url)
60
+ path = parsed.path.rstrip("/")
61
+
62
+ if path.endswith("/chat/completions"):
63
+ pass
64
+ elif path.endswith("/v1"):
65
+ path = f"{path}/chat/completions"
66
+ elif not path or path == "/":
67
+ path = "/v1/chat/completions"
68
+ else:
69
+ if "chat/completions" not in path:
70
+ path = f"{path}/chat/completions"
71
+
72
+ return urllib.parse.urlunparse(parsed._replace(path=path))
73
+
74
+
75
+ class LLMClient:
76
+ def __init__(self, api_key: str, endpoint: str, model: str):
77
+ self.api_key = api_key
78
+ self.endpoint_url = resolve_chat_url(endpoint)
79
+ self.model = model
80
+
81
+ def stream_chat(
82
+ self, messages: List[Dict[str, str]], stop_event: Optional[dict] = None
83
+ ) -> Generator[str, None, None]:
84
+ """Stream chat completions from OpenAI-compatible API."""
85
+ headers = {
86
+ "Content-Type": "application/json",
87
+ "Accept": "text/event-stream",
88
+ }
89
+ if self.api_key:
90
+ headers["Authorization"] = f"Bearer {self.api_key}"
91
+ headers["api-key"] = self.api_key # For Azure OpenAI compatibility
92
+
93
+ payload = {
94
+ "model": self.model,
95
+ "messages": messages,
96
+ "stream": True,
97
+ }
98
+
99
+ data = json.dumps(payload).encode("utf-8")
100
+ req = urllib.request.Request(self.endpoint_url, data=data, headers=headers, method="POST")
101
+
102
+ try:
103
+ with urllib.request.urlopen(req, timeout=60) as resp:
104
+ for line in resp:
105
+ if stop_event and stop_event.get("stop"):
106
+ break
107
+ line = line.decode("utf-8", errors="replace").strip()
108
+ if not line or line.startswith(":"):
109
+ continue
110
+ if line.startswith("data: "):
111
+ content_str = line[6:].strip()
112
+ if content_str == "[DONE]":
113
+ break
114
+ try:
115
+ chunk = json.loads(content_str)
116
+ choices = chunk.get("choices", [])
117
+ if choices:
118
+ delta = choices[0].get("delta", {})
119
+ text = delta.get("content", "")
120
+ if text:
121
+ yield text
122
+ except json.JSONDecodeError:
123
+ continue
124
+ except urllib.error.HTTPError as e:
125
+ err_body = e.read().decode("utf-8", errors="replace")
126
+ try:
127
+ err_json = json.loads(err_body)
128
+ msg = err_json.get("error", {}).get("message", err_body)
129
+ except Exception:
130
+ msg = err_body
131
+ raise RuntimeError(f"HTTP {e.code} Error: {msg}") from e
132
+ except urllib.error.URLError as e:
133
+ raise RuntimeError(f"Connection Error: {e.reason}") from e
134
+
135
+
136
+ class REPL:
137
+ def __init__(self, api_key: str, endpoint: str, model: str, system_prompt: str):
138
+ self.api_key = api_key
139
+ self.endpoint = endpoint
140
+ self.model = model
141
+ self.system_prompt = system_prompt
142
+ self.client = LLMClient(api_key=self.api_key, endpoint=self.endpoint, model=self.model)
143
+ self.history: List[Dict[str, str]] = []
144
+ self.history_file = os.path.expanduser("~/.askllm_history")
145
+ self._init_readline()
146
+
147
+ def _init_readline(self):
148
+ try:
149
+ readline.parse_and_bind("tab: complete")
150
+ if os.path.exists(self.history_file):
151
+ try:
152
+ readline.read_history_file(self.history_file)
153
+ except Exception:
154
+ pass
155
+ readline.set_history_length(1000)
156
+ atexit.register(self._save_history)
157
+ except Exception:
158
+ pass
159
+
160
+ def _save_history(self):
161
+ try:
162
+ readline.write_history_file(self.history_file)
163
+ except Exception:
164
+ pass
165
+
166
+ def reset_conversation(self):
167
+ self.history.clear()
168
+ print(f"{COLOR_DIM}Conversation history reset.{COLOR_RESET}")
169
+
170
+ def print_help(self):
171
+ print(f"""
172
+ {COLOR_BOLD}Commands:{COLOR_RESET}
173
+ {COLOR_CYAN}/help{COLOR_RESET} Show this help message
174
+ {COLOR_CYAN}/clear{COLOR_RESET}, {COLOR_CYAN}/reset{COLOR_RESET} Clear current conversation history
175
+ {COLOR_CYAN}/model [name]{COLOR_RESET} Show or switch current model
176
+ {COLOR_CYAN}/system [prompt]{COLOR_RESET} Show or update system prompt
177
+ {COLOR_CYAN}/history{COLOR_RESET} Show current session history
178
+ {COLOR_CYAN}/endpoint{COLOR_RESET} Show current API endpoint
179
+ {COLOR_CYAN}/paste{COLOR_RESET} Start multiline paste mode (finish with empty line or EOF)
180
+ {COLOR_CYAN}/quit{COLOR_RESET}, {COLOR_CYAN}/exit{COLOR_RESET} Exit REPL (or press Ctrl+C / Ctrl+D on an empty line)
181
+
182
+ {COLOR_BOLD}Tips:{COLOR_RESET}
183
+ - Type {COLOR_CYAN}\\\\{COLOR_RESET} at the end of a line or triple quotes {COLOR_CYAN}\"\"\"{COLOR_RESET} for multiline input.
184
+ - Press {COLOR_CYAN}Ctrl+C{COLOR_RESET} during streaming to cancel generation.
185
+ - Press {COLOR_CYAN}Ctrl+C{COLOR_RESET} or {COLOR_CYAN}Ctrl+D{COLOR_RESET} at the start of a prompt to exit.
186
+ """)
187
+
188
+ def handle_command(self, cmd: str) -> bool:
189
+ """Handle slash command. Returns True if handled, False otherwise."""
190
+ parts = cmd.strip().split(maxsplit=1)
191
+ action = parts[0].lower()
192
+ arg = parts[1].strip() if len(parts) > 1 else ""
193
+
194
+ if action in ("/quit", "/exit"):
195
+ print(f"{COLOR_DIM}Goodbye!{COLOR_RESET}")
196
+ sys.exit(0)
197
+ elif action in ("/help", "/?"):
198
+ self.print_help()
199
+ return True
200
+ elif action in ("/clear", "/reset"):
201
+ self.reset_conversation()
202
+ return True
203
+ elif action == "/model":
204
+ if arg:
205
+ self.model = arg
206
+ self.client.model = arg
207
+ print(f"{COLOR_DIM}Model switched to:{COLOR_RESET} {COLOR_BOLD}{self.model}{COLOR_RESET}")
208
+ else:
209
+ print(f"{COLOR_DIM}Current model:{COLOR_RESET} {COLOR_BOLD}{self.model}{COLOR_RESET}")
210
+ return True
211
+ elif action == "/endpoint":
212
+ print(f"{COLOR_DIM}Endpoint URL:{COLOR_RESET} {COLOR_BOLD}{self.client.endpoint_url}{COLOR_RESET}")
213
+ return True
214
+ elif action == "/system":
215
+ if arg:
216
+ self.system_prompt = arg
217
+ print(f"{COLOR_DIM}System prompt updated to:{COLOR_RESET}\n{self.system_prompt}")
218
+ else:
219
+ print(f"{COLOR_DIM}Current system prompt:{COLOR_RESET}\n{self.system_prompt}")
220
+ return True
221
+ elif action == "/history":
222
+ if not self.history:
223
+ print(f"{COLOR_DIM}No conversation history.{COLOR_RESET}")
224
+ else:
225
+ print(f"{COLOR_BOLD}--- Session History ---{COLOR_RESET}")
226
+ for msg in self.history:
227
+ role = msg["role"].upper()
228
+ color = COLOR_CYAN if role == "USER" else COLOR_GREEN
229
+ print(f"{color}{role}:{COLOR_RESET} {msg['content']}\n")
230
+ return True
231
+ elif action == "/paste":
232
+ self.read_paste_mode()
233
+ return True
234
+ else:
235
+ print(f"{COLOR_RED}Unknown command: {action}. Type /help for assistance.{COLOR_RESET}")
236
+ return True
237
+
238
+ def read_paste_mode(self):
239
+ print(f"{COLOR_DIM}Entering multiline paste mode. Press Enter twice or Ctrl+D on empty line to submit:{COLOR_RESET}")
240
+ lines = []
241
+ while True:
242
+ try:
243
+ line = input(f"{COLOR_DIM}... {COLOR_RESET}")
244
+ if line == "" and lines and lines[-1] == "":
245
+ lines.pop()
246
+ break
247
+ lines.append(line)
248
+ except EOFError:
249
+ break
250
+ except KeyboardInterrupt:
251
+ print(f"\n{COLOR_DIM}[Paste cancelled]{COLOR_RESET}")
252
+ return
253
+ text = "\n".join(lines).strip()
254
+ if text:
255
+ self.send_prompt(text)
256
+
257
+ def send_prompt(self, user_text: str):
258
+ # Build message history for the request
259
+ messages = []
260
+ if self.system_prompt:
261
+ messages.append({"role": "system", "content": self.system_prompt})
262
+ messages.extend(self.history)
263
+ messages.append({"role": "user", "content": user_text})
264
+
265
+ stop_event = {"stop": False}
266
+
267
+ def sigint_handler(sig, frame):
268
+ stop_event["stop"] = True
269
+ print(f"\n{COLOR_YELLOW}[Interrupted by user]{COLOR_RESET}")
270
+
271
+ orig_handler = signal.signal(signal.SIGINT, sigint_handler)
272
+
273
+ sys.stdout.write(f"\n{COLOR_GREEN}{COLOR_BOLD}AI:{COLOR_RESET} ")
274
+ sys.stdout.flush()
275
+
276
+ ai_response_chunks = []
277
+ try:
278
+ for chunk in self.client.stream_chat(messages, stop_event=stop_event):
279
+ if stop_event["stop"]:
280
+ break
281
+ sys.stdout.write(chunk)
282
+ sys.stdout.flush()
283
+ ai_response_chunks.append(chunk)
284
+ sys.stdout.write("\n\n")
285
+ sys.stdout.flush()
286
+ except RuntimeError as e:
287
+ sys.stdout.write("\n")
288
+ print(f"{COLOR_RED}Error: {e}{COLOR_RESET}\n")
289
+ return
290
+ except Exception as e:
291
+ sys.stdout.write("\n")
292
+ print(f"{COLOR_RED}Unexpected error: {e}{COLOR_RESET}\n")
293
+ return
294
+ finally:
295
+ signal.signal(signal.SIGINT, orig_handler)
296
+
297
+ ai_full_text = "".join(ai_response_chunks).strip()
298
+ if ai_full_text:
299
+ self.history.append({"role": "user", "content": user_text})
300
+ self.history.append({"role": "assistant", "content": ai_full_text})
301
+
302
+ def run(self):
303
+ print(f"{COLOR_BOLD}=== AskLLM REPL ==={COLOR_RESET}")
304
+ print(f"{COLOR_DIM}Endpoint:{COLOR_RESET} {COLOR_CYAN}{self.client.endpoint_url}{COLOR_RESET}")
305
+ print(f"{COLOR_DIM}Model: {COLOR_RESET} {COLOR_CYAN}{self.model}{COLOR_RESET}")
306
+ if not self.api_key:
307
+ print(f"{COLOR_YELLOW}Warning: OPENAI_API_KEY is not set.{COLOR_RESET}")
308
+ print(f"{COLOR_DIM}Type {COLOR_RESET}{COLOR_CYAN}/help{COLOR_RESET}{COLOR_DIM} for commands, {COLOR_RESET}{COLOR_CYAN}Ctrl+C{COLOR_RESET}{COLOR_DIM} or {COLOR_RESET}{COLOR_CYAN}Ctrl+D{COLOR_RESET}{COLOR_DIM} at beginning of line to exit.{COLOR_RESET}\n")
309
+
310
+ while True:
311
+ try:
312
+ prompt_line = input(f"{COLOR_BOLD}{COLOR_CYAN}>>> {COLOR_RESET}")
313
+ except EOFError:
314
+ # Ctrl+D at beginning of line quits
315
+ print(f"\n{COLOR_DIM}Goodbye!{COLOR_RESET}")
316
+ break
317
+ except KeyboardInterrupt:
318
+ # Check readline buffer
319
+ buf = readline.get_line_buffer()
320
+ if not buf:
321
+ # Ctrl+C at beginning of line quits
322
+ print(f"\n{COLOR_DIM}Goodbye!{COLOR_RESET}")
323
+ break
324
+ else:
325
+ # Ctrl+C with text in buffer clears line and gives new prompt
326
+ print("^C")
327
+ continue
328
+
329
+ user_text = prompt_line.strip()
330
+ if not user_text:
331
+ continue
332
+
333
+ # Check if entering triple quotes multiline mode
334
+ if user_text.startswith('"""') or user_text.startswith("'''"):
335
+ quote_type = user_text[:3]
336
+ remaining = user_text[3:]
337
+ lines = []
338
+ if remaining.endswith(quote_type) and len(remaining) >= 3:
339
+ user_text = remaining[:-3].strip()
340
+ else:
341
+ if remaining:
342
+ lines.append(remaining)
343
+ while True:
344
+ try:
345
+ cont_line = input(f"{COLOR_DIM}... {COLOR_RESET}")
346
+ if cont_line.endswith(quote_type):
347
+ lines.append(cont_line[:-3])
348
+ break
349
+ lines.append(cont_line)
350
+ except (EOFError, KeyboardInterrupt):
351
+ print(f"\n{COLOR_DIM}[Input cancelled]{COLOR_RESET}")
352
+ lines = None
353
+ break
354
+ if lines is None:
355
+ continue
356
+ user_text = "\n".join(lines).strip()
357
+
358
+ # Check for trailing backslash continuation
359
+ elif user_text.endswith("\\"):
360
+ lines = [user_text[:-1]]
361
+ cancelled = False
362
+ while True:
363
+ try:
364
+ cont_line = input(f"{COLOR_DIM}... {COLOR_RESET}")
365
+ if cont_line.endswith("\\"):
366
+ lines.append(cont_line[:-1])
367
+ else:
368
+ lines.append(cont_line)
369
+ break
370
+ except (EOFError, KeyboardInterrupt):
371
+ print(f"\n{COLOR_DIM}[Input cancelled]{COLOR_RESET}")
372
+ cancelled = True
373
+ break
374
+ if cancelled:
375
+ continue
376
+ user_text = "\n".join(lines).strip()
377
+
378
+ if not user_text:
379
+ continue
380
+
381
+ if user_text.startswith("/"):
382
+ self.handle_command(user_text)
383
+ continue
384
+
385
+ self.send_prompt(user_text)
386
+
387
+
388
+ def parse_args(args=None):
389
+ import argparse
390
+ parser = argparse.ArgumentParser(prog="askllm", description="AskLLM - Lightweight Python REPL for LLMs")
391
+ parser.add_argument(
392
+ "-v", "--version",
393
+ action="version",
394
+ version=f"%(prog)s {__version__}"
395
+ )
396
+ parser.add_argument(
397
+ "-m", "--model",
398
+ default=get_default_env(["OPENAI_MODEL", "OPENAI_MODEL_NAME", "MODEL"], "gpt-4o-mini"),
399
+ help="LLM model name (env: OPENAI_MODEL, default: gpt-4o-mini)"
400
+ )
401
+ parser.add_argument(
402
+ "-e", "--endpoint",
403
+ default=get_default_env(["OPENAI_BASE_URL", "OPENAI_ENDPOINT", "OPENAI_API_BASE"], "https://api.openai.com/v1"),
404
+ help="API Base URL / Endpoint (env: OPENAI_BASE_URL, OPENAI_ENDPOINT, OPENAI_API_BASE)"
405
+ )
406
+ parser.add_argument(
407
+ "-k", "--api-key",
408
+ default=get_default_env(["OPENAI_API_KEY"], ""),
409
+ help="API Key (env: OPENAI_API_KEY)"
410
+ )
411
+ parser.add_argument(
412
+ "-s", "--system",
413
+ default=get_default_env(["OPENAI_SYSTEM_PROMPT"], "You are a helpful assistant."),
414
+ help="System prompt (env: OPENAI_SYSTEM_PROMPT)"
415
+ )
416
+ return parser.parse_args(args)
417
+
418
+
419
+ def main(argv=None):
420
+ args = parse_args(argv)
421
+ repl = REPL(
422
+ api_key=args.api_key,
423
+ endpoint=args.endpoint,
424
+ model=args.model,
425
+ system_prompt=args.system,
426
+ )
427
+ repl.run()
428
+
429
+
430
+ if __name__ == "__main__":
431
+ main()
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.4
2
+ Name: askllm-cli
3
+ Version: 0.1.0
4
+ Summary: Lightweight OpenAI-compatible terminal REPL
5
+ Author: AskLLM
6
+ License: MIT
7
+ Keywords: llm,repl,cli,openai,terminal,ai
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Communications :: Chat
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.8
22
+ Description-Content-Type: text/markdown
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.0; extra == "dev"
25
+
26
+ # AskLLM
27
+
28
+ A lightweight, zero-dependency Python REPL for chatting with LLMs via any OpenAI-compatible API.
29
+
30
+ Designed to be installed via `pip install askllm-cli` (or `pipx install askllm-cli`), and developed using **Podman** with `python:3.12-slim` (or run directly with Python 3).
31
+
32
+ ---
33
+
34
+ ## Features
35
+
36
+ - **Zero Third-Party Runtime Dependencies:** Uses standard library only (`urllib.request`, `json`, `readline`).
37
+ - **Standard Python Package:** Modern PEP 517/621 packaging (`pyproject.toml`) published as `askllm-cli` on PyPI.
38
+ - **Podman Development Ready:** Built-in development workflow using `python:3.12-slim` (`./dev.sh`).
39
+ - **Environment Variable Auto-Detection:** Automatically picks up standard OpenAI / Azure / local LLM variables.
40
+ - **Streaming Responses:** Real-time token streaming via Server-Sent Events (SSE).
41
+ - **Graceful Signal Handling:**
42
+ - `Ctrl+C` or `Ctrl+D` at the beginning of the line exits the REPL.
43
+ - `Ctrl+C` with text in the buffer cancels the line.
44
+ - `Ctrl+C` during response streaming halts generation cleanly without exiting.
45
+ - **Multi-Turn Chat History:** Maintains conversation context across turns within the session.
46
+ - **Persistent Input History:** Readline history saved across runs in `~/.askllm_history`.
47
+ - **Multiline Input Support:**
48
+ - Triple quotes `"""` ... `"""`
49
+ - Trailing backslash `\`
50
+ - `/paste` command
51
+
52
+ ---
53
+
54
+ ## Development with Podman (`python:3.12-slim`)
55
+
56
+ No local Python installation or host dependencies are required. A complete development environment is provided via Podman:
57
+
58
+ ### 1. Interactive Development Shell
59
+ Drop into an interactive bash shell running inside `python:3.12-slim` with AskLLM installed in editable mode (`pip install -e .`):
60
+
61
+ ```bash
62
+ ./dev.sh
63
+ ```
64
+
65
+ Inside the shell:
66
+ ```bash
67
+ # Run AskLLM directly
68
+ askllm --help
69
+
70
+ # Run tests
71
+ pytest
72
+
73
+ # Test interactive python
74
+ python3 -c "import askllm; print(askllm.__version__)"
75
+ ```
76
+
77
+ Any changes made to files in `src/askllm` on your host are immediately reflected inside the container!
78
+
79
+ ### 2. Run Tests in Podman
80
+ Run the test suite inside the `python:3.12-slim` container:
81
+
82
+ ```bash
83
+ ./dev.sh test
84
+ ```
85
+
86
+ ### 3. Run AskLLM via Podman
87
+ Run AskLLM using Podman directly from the host:
88
+
89
+ ```bash
90
+ ./askllm
91
+ ```
92
+ or with arguments:
93
+ ```bash
94
+ ./askllm --model llama3.2 --endpoint http://localhost:11434/v1
95
+ ```
96
+
97
+ ### 4. Dev Container (VS Code / IDEs)
98
+ Open this repository in VS Code or any editor supporting Dev Containers to develop seamlessly inside the `python:3.12-slim` container.
99
+
100
+ ---
101
+
102
+ ## Installation via `pip` / `pipx`
103
+
104
+ Install AskLLM from PyPI:
105
+
106
+ ```bash
107
+ # Recommended for CLI tools
108
+ pipx install askllm-cli
109
+
110
+ # Or via standard pip
111
+ pip install askllm-cli
112
+ ```
113
+
114
+ Or install locally in editable mode in any Python virtual environment:
115
+
116
+ ```bash
117
+ # Editable install
118
+ pip install -e .
119
+
120
+ # With dev dependencies (pytest)
121
+ pip install -e ".[dev]"
122
+ ```
123
+
124
+ Once installed, the `askllm` command is directly available:
125
+
126
+ ```bash
127
+ askllm --help
128
+ ```
129
+
130
+ Or run as a module:
131
+ ```bash
132
+ python3 -m askllm
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Production Container
138
+
139
+ Build and run the production image using Podman:
140
+
141
+ ```bash
142
+ podman build -t askllm .
143
+ podman run --rm -it --network=host -e OPENAI_API_KEY askllm
144
+ ```
145
+
146
+ ---
147
+
148
+ ## Environment Variables
149
+
150
+ AskLLM automatically checks for these environment variables:
151
+
152
+ | Variable | Description | Default |
153
+ | :--- | :--- | :--- |
154
+ | `OPENAI_API_KEY` | Your API key | *(empty / none)* |
155
+ | `OPENAI_ENDPOINT` or `OPENAI_BASE_URL` or `OPENAI_API_BASE` | API base URL or chat endpoint | `https://api.openai.com/v1` |
156
+ | `OPENAI_MODEL` | Default model name | `gpt-4o-mini` |
157
+ | `OPENAI_SYSTEM_PROMPT` | Custom system prompt | `"You are a helpful assistant."` |
158
+
159
+ ---
160
+
161
+ ## Using with Local LLMs (Ollama, LM Studio, vLLM, etc.)
162
+
163
+ Because `./askllm` and `./dev.sh` use host networking (`--network=host`), you can connect directly to local servers:
164
+
165
+ #### Ollama:
166
+ ```bash
167
+ OPENAI_ENDPOINT="http://localhost:11434/v1" OPENAI_MODEL="llama3.2" ./askllm
168
+ ```
169
+
170
+ #### LM Studio / LocalAI / vLLM:
171
+ ```bash
172
+ OPENAI_ENDPOINT="http://localhost:1234/v1" OPENAI_MODEL="local-model" ./askllm
173
+ ```
174
+
175
+ ---
176
+
177
+ ## REPL Slash Commands
178
+
179
+ Inside the REPL, type `/` to access built-in commands:
180
+
181
+ | Command | Action |
182
+ | :--- | :--- |
183
+ | `/help` | Display command help and tips |
184
+ | `/clear` or `/reset` | Clear session conversation history |
185
+ | `/model [name]` | Show or dynamically switch model |
186
+ | `/endpoint` | Show currently configured endpoint URL |
187
+ | `/system [prompt]` | Show or update the system prompt |
188
+ | `/history` | Show full message history for the session |
189
+ | `/paste` | Enter multiline paste mode |
190
+ | `/exit` or `/quit` | Exit the REPL |
191
+
192
+ ---
193
+
194
+ ## Command-Line Options
195
+
196
+ ```
197
+ usage: askllm [-h] [-v] [-m MODEL] [-e ENDPOINT] [-k API_KEY] [-s SYSTEM]
198
+
199
+ options:
200
+ -h, --help show this help message and exit
201
+ -v, --version show program's version number and exit
202
+ -m MODEL, --model MODEL
203
+ LLM model name (env: OPENAI_MODEL, default: gpt-4o-mini)
204
+ -e ENDPOINT, --endpoint ENDPOINT
205
+ API Base URL / Endpoint (env: OPENAI_BASE_URL, OPENAI_ENDPOINT, OPENAI_API_BASE)
206
+ -k API_KEY, --api-key API_KEY
207
+ API Key (env: OPENAI_API_KEY)
208
+ -s SYSTEM, --system SYSTEM
209
+ System prompt (env: OPENAI_SYSTEM_PROMPT)
210
+ ```
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/askllm/__init__.py
4
+ src/askllm/__main__.py
5
+ src/askllm/cli.py
6
+ src/askllm_cli.egg-info/PKG-INFO
7
+ src/askllm_cli.egg-info/SOURCES.txt
8
+ src/askllm_cli.egg-info/dependency_links.txt
9
+ src/askllm_cli.egg-info/entry_points.txt
10
+ src/askllm_cli.egg-info/requires.txt
11
+ src/askllm_cli.egg-info/top_level.txt
12
+ tests/test_askllm.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ askllm = askllm.cli:main
@@ -0,0 +1,3 @@
1
+
2
+ [dev]
3
+ pytest>=7.0
@@ -0,0 +1,99 @@
1
+ import os
2
+ import unittest
3
+ from unittest.mock import patch
4
+
5
+ from askllm import __version__, LLMClient, REPL
6
+ from askllm.cli import resolve_chat_url, get_default_env, parse_args
7
+
8
+
9
+ class TestAskLLM(unittest.TestCase):
10
+ def test_version(self):
11
+ self.assertEqual(__version__, "0.1.0")
12
+
13
+ def test_resolve_chat_url(self):
14
+ self.assertEqual(
15
+ resolve_chat_url(""),
16
+ "https://api.openai.com/v1/chat/completions"
17
+ )
18
+ self.assertEqual(
19
+ resolve_chat_url("https://api.openai.com/v1"),
20
+ "https://api.openai.com/v1/chat/completions"
21
+ )
22
+ self.assertEqual(
23
+ resolve_chat_url("https://api.openai.com/v1/"),
24
+ "https://api.openai.com/v1/chat/completions"
25
+ )
26
+ self.assertEqual(
27
+ resolve_chat_url("https://api.openai.com/v1/chat/completions"),
28
+ "https://api.openai.com/v1/chat/completions"
29
+ )
30
+ self.assertEqual(
31
+ resolve_chat_url("http://localhost:11434/v1"),
32
+ "http://localhost:11434/v1/chat/completions"
33
+ )
34
+ self.assertEqual(
35
+ resolve_chat_url("http://localhost:1234"),
36
+ "http://localhost:1234/v1/chat/completions"
37
+ )
38
+
39
+ def test_get_default_env(self):
40
+ with patch.dict(os.environ, {"TEST_KEY_B": "val_b"}, clear=True):
41
+ self.assertEqual(
42
+ get_default_env(["TEST_KEY_A", "TEST_KEY_B"], "fallback"),
43
+ "val_b"
44
+ )
45
+ self.assertEqual(
46
+ get_default_env(["TEST_KEY_NONE"], "fallback"),
47
+ "fallback"
48
+ )
49
+
50
+ def test_parse_args_defaults(self):
51
+ with patch.dict(os.environ, {}, clear=True):
52
+ args = parse_args([])
53
+ self.assertEqual(args.model, "gpt-4o-mini")
54
+ self.assertEqual(args.endpoint, "https://api.openai.com/v1")
55
+ self.assertEqual(args.api_key, "")
56
+ self.assertEqual(args.system, "You are a helpful assistant.")
57
+
58
+ def test_parse_args_custom(self):
59
+ args = parse_args([
60
+ "-m", "llama3.2",
61
+ "-e", "http://localhost:11434/v1",
62
+ "-k", "my-secret-key",
63
+ "-s", "Custom system prompt"
64
+ ])
65
+ self.assertEqual(args.model, "llama3.2")
66
+ self.assertEqual(args.endpoint, "http://localhost:11434/v1")
67
+ self.assertEqual(args.api_key, "my-secret-key")
68
+ self.assertEqual(args.system, "Custom system prompt")
69
+
70
+ def test_repl_commands(self):
71
+ repl = REPL(
72
+ api_key="test-key",
73
+ endpoint="http://localhost:1234/v1",
74
+ model="gpt-4o",
75
+ system_prompt="Initial system prompt"
76
+ )
77
+ # Add a fake conversation history
78
+ repl.history.append({"role": "user", "content": "hello"})
79
+ self.assertEqual(len(repl.history), 1)
80
+
81
+ # /reset command
82
+ handled = repl.handle_command("/reset")
83
+ self.assertTrue(handled)
84
+ self.assertEqual(len(repl.history), 0)
85
+
86
+ # /model command
87
+ handled = repl.handle_command("/model claude-3-opus")
88
+ self.assertTrue(handled)
89
+ self.assertEqual(repl.model, "claude-3-opus")
90
+ self.assertEqual(repl.client.model, "claude-3-opus")
91
+
92
+ # /system command
93
+ handled = repl.handle_command("/system New prompt")
94
+ self.assertTrue(handled)
95
+ self.assertEqual(repl.system_prompt, "New prompt")
96
+
97
+
98
+ if __name__ == "__main__":
99
+ unittest.main()