pylolm 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.
- pylolm-0.1.0/LICENSE +21 -0
- pylolm-0.1.0/PKG-INFO +174 -0
- pylolm-0.1.0/README.md +145 -0
- pylolm-0.1.0/pyproject.toml +53 -0
- pylolm-0.1.0/setup.cfg +4 -0
- pylolm-0.1.0/src/pylolm/__init__.py +36 -0
- pylolm-0.1.0/src/pylolm/__main__.py +168 -0
- pylolm-0.1.0/src/pylolm/_download.py +184 -0
- pylolm-0.1.0/src/pylolm/_lib.py +155 -0
- pylolm-0.1.0/src/pylolm/errors.py +25 -0
- pylolm-0.1.0/src/pylolm/llama.py +441 -0
- pylolm-0.1.0/src/pylolm/native/CMakeLists.txt +28 -0
- pylolm-0.1.0/src/pylolm/native/LLAMA_CPP_REF +1 -0
- pylolm-0.1.0/src/pylolm/native/build_android_ndk.sh +54 -0
- pylolm-0.1.0/src/pylolm/native/build_termux.sh +45 -0
- pylolm-0.1.0/src/pylolm/native/pylolm_shim.c +316 -0
- pylolm-0.1.0/src/pylolm/py.typed +0 -0
- pylolm-0.1.0/src/pylolm.egg-info/PKG-INFO +174 -0
- pylolm-0.1.0/src/pylolm.egg-info/SOURCES.txt +24 -0
- pylolm-0.1.0/src/pylolm.egg-info/dependency_links.txt +1 -0
- pylolm-0.1.0/src/pylolm.egg-info/entry_points.txt +2 -0
- pylolm-0.1.0/src/pylolm.egg-info/requires.txt +6 -0
- pylolm-0.1.0/src/pylolm.egg-info/top_level.txt +1 -0
- pylolm-0.1.0/tests/test_download.py +106 -0
- pylolm-0.1.0/tests/test_integration.py +74 -0
- pylolm-0.1.0/tests/test_pure.py +91 -0
pylolm-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AuraWay
|
|
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.
|
pylolm-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pylolm
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LoLm: run GGUF language models on-device on Android from Python (llama.cpp bindings for Kivy, Flet and Termux).
|
|
5
|
+
Author-email: AuraWay <auraway.dev@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aura-way/pylolm
|
|
8
|
+
Project-URL: Issues, https://github.com/aura-way/pylolm/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/aura-way/pylolm/blob/main/CHANGELOG.md
|
|
10
|
+
Keywords: llm,llama.cpp,gguf,android,termux,kivy,flet,on-device,local-llm
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: Android
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: C
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff; extra == "dev"
|
|
26
|
+
Requires-Dist: build; extra == "dev"
|
|
27
|
+
Requires-Dist: twine; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# pylolm (LoLm)
|
|
31
|
+
|
|
32
|
+
**Local LLMs on Android, from Python.** An Android-friendly take on `llama-cpp-python`, built on
|
|
33
|
+
[llama.cpp](https://github.com/ggml-org/llama.cpp) and designed for **Kivy**, **Flet** and **Termux**.
|
|
34
|
+
|
|
35
|
+
- Zero Python dependencies: a thin `ctypes` layer over a small C shim.
|
|
36
|
+
- `llama-cpp-python`-style API: `Llama(...)`, `create_chat_completion(...)`, `create_completion(...)`.
|
|
37
|
+
- Streaming generators, stop strings, a `stop()` button for your UI.
|
|
38
|
+
- Multi-turn chat that reuses the KV cache, so long conversations stay fast.
|
|
39
|
+
- Aimed at small GGUF models (0.5B-2B) on phones.
|
|
40
|
+
|
|
41
|
+
> **Status: alpha (0.1.0).** Text generation only. TTS / STT / OCR are on the roadmap.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install pylolm
|
|
47
|
+
pylolm get-libs # downloads the prebuilt native libraries (seconds)
|
|
48
|
+
pylolm doctor # checks that everything loads
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`get-libs` fetches the Android arm64 libraries attached to the matching GitHub Release, checks their
|
|
52
|
+
SHA-256 checksum, and installs them to `~/.pylolm/lib`. These are built by CI to run on a wide range
|
|
53
|
+
of phones.
|
|
54
|
+
|
|
55
|
+
Prefer to compile for your exact device, or `get-libs` is not available for your setup?
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pkg install cmake clang git
|
|
59
|
+
pylolm build-native # clones llama.cpp and builds on the device (slow: ~30 min)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Libraries are searched in `$PYLOLM_LIB_DIR`, then `pylolm/lib/`, then `~/.pylolm/lib/`.
|
|
63
|
+
|
|
64
|
+
## Quick start
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from pylolm import Llama
|
|
68
|
+
|
|
69
|
+
llm = Llama("/sdcard/models/Qwen2.5-0.5B-Instruct-Q4_K_M.gguf", n_ctx=2048)
|
|
70
|
+
|
|
71
|
+
messages = [{"role": "user", "content": "Explain GGUF in two sentences."}]
|
|
72
|
+
|
|
73
|
+
# whole answer
|
|
74
|
+
out = llm.create_chat_completion(messages, max_tokens=200)
|
|
75
|
+
print(out["choices"][0]["message"]["content"])
|
|
76
|
+
|
|
77
|
+
# streaming
|
|
78
|
+
for chunk in llm.create_chat_completion(messages, max_tokens=200, stream=True):
|
|
79
|
+
piece = chunk["choices"][0]["delta"].get("content")
|
|
80
|
+
if piece:
|
|
81
|
+
print(piece, end="", flush=True)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Try it from the terminal: `pylolm chat /path/to/model.gguf`.
|
|
85
|
+
|
|
86
|
+
## API
|
|
87
|
+
|
|
88
|
+
| Call | Notes |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `Llama(model_path, n_ctx=2048, n_threads=None, verbose=False)` | `n_threads` defaults to `min(4, cpu_count)` |
|
|
91
|
+
| `create_chat_completion(messages, max_tokens=256, temperature=0.7, top_p=0.95, top_k=40, min_p=0.05, repeat_penalty=1.0, seed=None, stop=None, stream=False, chat_template=None)` | OpenAI-style dict, or chunk iterator with `stream=True` |
|
|
92
|
+
| `create_completion(prompt, ...)` / `llm(prompt, ...)` | raw text completion |
|
|
93
|
+
| `stop()` | stop the running generation from another thread |
|
|
94
|
+
| `reset()` | clear the KV cache (forget the conversation) |
|
|
95
|
+
| `count_tokens(text)` | tokens used by a string |
|
|
96
|
+
| `close()` / `with Llama(...) as llm:` | free the model |
|
|
97
|
+
|
|
98
|
+
Errors: `LibraryNotFoundError`, `ModelLoadError`, `ContextOverflowError`, `GenerationError`
|
|
99
|
+
(all subclass `PyLoLmError`).
|
|
100
|
+
|
|
101
|
+
`usage` in a non-streaming result includes `cached_prompt_tokens`: how many prompt tokens were
|
|
102
|
+
reused from the previous turn.
|
|
103
|
+
|
|
104
|
+
**Chat templates.** The model's built-in template is applied through llama.cpp's template
|
|
105
|
+
support, which covers the common families (ChatML/Qwen, Llama 3, Gemma, ...) but is not a full
|
|
106
|
+
Jinja engine. If a newer model's template is rejected, pass `chat_template="chatml"` (or another
|
|
107
|
+
known name) or build the prompt yourself and call `create_completion`.
|
|
108
|
+
|
|
109
|
+
## Using it in apps
|
|
110
|
+
|
|
111
|
+
**UI threads.** Generation blocks, so run it off the UI thread. A streaming generator is easiest:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import threading
|
|
115
|
+
|
|
116
|
+
def worker():
|
|
117
|
+
for chunk in llm.create_chat_completion(messages, stream=True):
|
|
118
|
+
text = chunk["choices"][0]["delta"].get("content")
|
|
119
|
+
if text:
|
|
120
|
+
Clock.schedule_once(lambda dt, t=text: append_to_label(t)) # Kivy
|
|
121
|
+
# page.run_thread / page.update(...) for Flet
|
|
122
|
+
|
|
123
|
+
threading.Thread(target=worker, daemon=True).start()
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Call `llm.stop()` from your Stop button; breaking out of the loop also stops generation.
|
|
127
|
+
|
|
128
|
+
**Flet.** `flet build apk` installs packages from PyPI using the Android platform tag, so the
|
|
129
|
+
`android_24_arm64_v8a` wheel is what ships the native libraries. This path needs on-device
|
|
130
|
+
verification for your Flet/Python version.
|
|
131
|
+
|
|
132
|
+
**Kivy / python-for-android.** p4a does not consume Android wheels yet. For now, bundle the
|
|
133
|
+
`.so` files with your app and point `PYLOLM_LIB_DIR` at them before importing pylolm:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
import os
|
|
137
|
+
os.environ["PYLOLM_LIB_DIR"] = os.path.join(os.path.dirname(__file__), "pylolm_libs")
|
|
138
|
+
import pylolm
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
A proper p4a recipe is on the roadmap.
|
|
142
|
+
|
|
143
|
+
## Building from source
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
git clone https://github.com/auraway/pylolm && cd pylolm
|
|
147
|
+
pip install -e ".[dev]"
|
|
148
|
+
python -m pytest # pure-Python tests, no model needed
|
|
149
|
+
PYLOLM_MODEL=/path/model.gguf python -m pytest # + integration tests
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Native libraries: `src/pylolm/native/build_termux.sh` (on a phone) or `build_android_ndk.sh`
|
|
153
|
+
(cross-compile). The llama.cpp version is pinned in `src/pylolm/native/LLAMA_CPP_REF`.
|
|
154
|
+
|
|
155
|
+
## Releasing to PyPI
|
|
156
|
+
|
|
157
|
+
1. Set `LLAMA_CPP_REF` to a llama.cpp tag/commit you have tested.
|
|
158
|
+
2. Bump `version` in `pyproject.toml` and add a `CHANGELOG.md` entry.
|
|
159
|
+
3. On PyPI, add a [trusted publisher](https://docs.pypi.org/trusted-publishers/) for this repo
|
|
160
|
+
(workflow `release.yml`, environment `pypi`).
|
|
161
|
+
4. `git tag v0.1.0 && git push --tags`. CI builds the sdist, a pure wheel, and the Android arm64
|
|
162
|
+
wheel, publishes them to PyPI, and attaches `pylolm-libs-android-arm64-v8a.tar.gz` (+ `.sha256`)
|
|
163
|
+
to the GitHub Release for `pylolm get-libs`. The tag must match the version in `pyproject.toml`.
|
|
164
|
+
|
|
165
|
+
## Roadmap
|
|
166
|
+
|
|
167
|
+
- p4a recipe for Kivy; verified Flet build
|
|
168
|
+
- whisper.cpp (speech-to-text) module
|
|
169
|
+
- TTS and OCR modules
|
|
170
|
+
- GPU backends (Vulkan/OpenCL) as an opt-in
|
|
171
|
+
|
|
172
|
+
## License
|
|
173
|
+
|
|
174
|
+
MIT. llama.cpp is MIT-licensed too; model licenses are your responsibility.
|
pylolm-0.1.0/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# pylolm (LoLm)
|
|
2
|
+
|
|
3
|
+
**Local LLMs on Android, from Python.** An Android-friendly take on `llama-cpp-python`, built on
|
|
4
|
+
[llama.cpp](https://github.com/ggml-org/llama.cpp) and designed for **Kivy**, **Flet** and **Termux**.
|
|
5
|
+
|
|
6
|
+
- Zero Python dependencies: a thin `ctypes` layer over a small C shim.
|
|
7
|
+
- `llama-cpp-python`-style API: `Llama(...)`, `create_chat_completion(...)`, `create_completion(...)`.
|
|
8
|
+
- Streaming generators, stop strings, a `stop()` button for your UI.
|
|
9
|
+
- Multi-turn chat that reuses the KV cache, so long conversations stay fast.
|
|
10
|
+
- Aimed at small GGUF models (0.5B-2B) on phones.
|
|
11
|
+
|
|
12
|
+
> **Status: alpha (0.1.0).** Text generation only. TTS / STT / OCR are on the roadmap.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install pylolm
|
|
18
|
+
pylolm get-libs # downloads the prebuilt native libraries (seconds)
|
|
19
|
+
pylolm doctor # checks that everything loads
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`get-libs` fetches the Android arm64 libraries attached to the matching GitHub Release, checks their
|
|
23
|
+
SHA-256 checksum, and installs them to `~/.pylolm/lib`. These are built by CI to run on a wide range
|
|
24
|
+
of phones.
|
|
25
|
+
|
|
26
|
+
Prefer to compile for your exact device, or `get-libs` is not available for your setup?
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pkg install cmake clang git
|
|
30
|
+
pylolm build-native # clones llama.cpp and builds on the device (slow: ~30 min)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Libraries are searched in `$PYLOLM_LIB_DIR`, then `pylolm/lib/`, then `~/.pylolm/lib/`.
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from pylolm import Llama
|
|
39
|
+
|
|
40
|
+
llm = Llama("/sdcard/models/Qwen2.5-0.5B-Instruct-Q4_K_M.gguf", n_ctx=2048)
|
|
41
|
+
|
|
42
|
+
messages = [{"role": "user", "content": "Explain GGUF in two sentences."}]
|
|
43
|
+
|
|
44
|
+
# whole answer
|
|
45
|
+
out = llm.create_chat_completion(messages, max_tokens=200)
|
|
46
|
+
print(out["choices"][0]["message"]["content"])
|
|
47
|
+
|
|
48
|
+
# streaming
|
|
49
|
+
for chunk in llm.create_chat_completion(messages, max_tokens=200, stream=True):
|
|
50
|
+
piece = chunk["choices"][0]["delta"].get("content")
|
|
51
|
+
if piece:
|
|
52
|
+
print(piece, end="", flush=True)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Try it from the terminal: `pylolm chat /path/to/model.gguf`.
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
| Call | Notes |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `Llama(model_path, n_ctx=2048, n_threads=None, verbose=False)` | `n_threads` defaults to `min(4, cpu_count)` |
|
|
62
|
+
| `create_chat_completion(messages, max_tokens=256, temperature=0.7, top_p=0.95, top_k=40, min_p=0.05, repeat_penalty=1.0, seed=None, stop=None, stream=False, chat_template=None)` | OpenAI-style dict, or chunk iterator with `stream=True` |
|
|
63
|
+
| `create_completion(prompt, ...)` / `llm(prompt, ...)` | raw text completion |
|
|
64
|
+
| `stop()` | stop the running generation from another thread |
|
|
65
|
+
| `reset()` | clear the KV cache (forget the conversation) |
|
|
66
|
+
| `count_tokens(text)` | tokens used by a string |
|
|
67
|
+
| `close()` / `with Llama(...) as llm:` | free the model |
|
|
68
|
+
|
|
69
|
+
Errors: `LibraryNotFoundError`, `ModelLoadError`, `ContextOverflowError`, `GenerationError`
|
|
70
|
+
(all subclass `PyLoLmError`).
|
|
71
|
+
|
|
72
|
+
`usage` in a non-streaming result includes `cached_prompt_tokens`: how many prompt tokens were
|
|
73
|
+
reused from the previous turn.
|
|
74
|
+
|
|
75
|
+
**Chat templates.** The model's built-in template is applied through llama.cpp's template
|
|
76
|
+
support, which covers the common families (ChatML/Qwen, Llama 3, Gemma, ...) but is not a full
|
|
77
|
+
Jinja engine. If a newer model's template is rejected, pass `chat_template="chatml"` (or another
|
|
78
|
+
known name) or build the prompt yourself and call `create_completion`.
|
|
79
|
+
|
|
80
|
+
## Using it in apps
|
|
81
|
+
|
|
82
|
+
**UI threads.** Generation blocks, so run it off the UI thread. A streaming generator is easiest:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import threading
|
|
86
|
+
|
|
87
|
+
def worker():
|
|
88
|
+
for chunk in llm.create_chat_completion(messages, stream=True):
|
|
89
|
+
text = chunk["choices"][0]["delta"].get("content")
|
|
90
|
+
if text:
|
|
91
|
+
Clock.schedule_once(lambda dt, t=text: append_to_label(t)) # Kivy
|
|
92
|
+
# page.run_thread / page.update(...) for Flet
|
|
93
|
+
|
|
94
|
+
threading.Thread(target=worker, daemon=True).start()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Call `llm.stop()` from your Stop button; breaking out of the loop also stops generation.
|
|
98
|
+
|
|
99
|
+
**Flet.** `flet build apk` installs packages from PyPI using the Android platform tag, so the
|
|
100
|
+
`android_24_arm64_v8a` wheel is what ships the native libraries. This path needs on-device
|
|
101
|
+
verification for your Flet/Python version.
|
|
102
|
+
|
|
103
|
+
**Kivy / python-for-android.** p4a does not consume Android wheels yet. For now, bundle the
|
|
104
|
+
`.so` files with your app and point `PYLOLM_LIB_DIR` at them before importing pylolm:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
import os
|
|
108
|
+
os.environ["PYLOLM_LIB_DIR"] = os.path.join(os.path.dirname(__file__), "pylolm_libs")
|
|
109
|
+
import pylolm
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
A proper p4a recipe is on the roadmap.
|
|
113
|
+
|
|
114
|
+
## Building from source
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
git clone https://github.com/auraway/pylolm && cd pylolm
|
|
118
|
+
pip install -e ".[dev]"
|
|
119
|
+
python -m pytest # pure-Python tests, no model needed
|
|
120
|
+
PYLOLM_MODEL=/path/model.gguf python -m pytest # + integration tests
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Native libraries: `src/pylolm/native/build_termux.sh` (on a phone) or `build_android_ndk.sh`
|
|
124
|
+
(cross-compile). The llama.cpp version is pinned in `src/pylolm/native/LLAMA_CPP_REF`.
|
|
125
|
+
|
|
126
|
+
## Releasing to PyPI
|
|
127
|
+
|
|
128
|
+
1. Set `LLAMA_CPP_REF` to a llama.cpp tag/commit you have tested.
|
|
129
|
+
2. Bump `version` in `pyproject.toml` and add a `CHANGELOG.md` entry.
|
|
130
|
+
3. On PyPI, add a [trusted publisher](https://docs.pypi.org/trusted-publishers/) for this repo
|
|
131
|
+
(workflow `release.yml`, environment `pypi`).
|
|
132
|
+
4. `git tag v0.1.0 && git push --tags`. CI builds the sdist, a pure wheel, and the Android arm64
|
|
133
|
+
wheel, publishes them to PyPI, and attaches `pylolm-libs-android-arm64-v8a.tar.gz` (+ `.sha256`)
|
|
134
|
+
to the GitHub Release for `pylolm get-libs`. The tag must match the version in `pyproject.toml`.
|
|
135
|
+
|
|
136
|
+
## Roadmap
|
|
137
|
+
|
|
138
|
+
- p4a recipe for Kivy; verified Flet build
|
|
139
|
+
- whisper.cpp (speech-to-text) module
|
|
140
|
+
- TTS and OCR modules
|
|
141
|
+
- GPU backends (Vulkan/OpenCL) as an opt-in
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
MIT. llama.cpp is MIT-licensed too; model licenses are your responsibility.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pylolm"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "LoLm: run GGUF language models on-device on Android from Python (llama.cpp bindings for Kivy, Flet and Termux)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
authors = [{ name = "AuraWay", email = "auraway.dev@gmail.com" }]
|
|
14
|
+
keywords = ["llm", "llama.cpp", "gguf", "android", "termux", "kivy", "flet", "on-device", "local-llm"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: Android",
|
|
19
|
+
"Operating System :: POSIX :: Linux",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
22
|
+
"Programming Language :: C",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = ["pytest>=7", "ruff", "build", "twine"]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
pylolm = "pylolm.__main__:main"
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/aura-way/pylolm"
|
|
36
|
+
Issues = "https://github.com/aura-way/pylolm/issues"
|
|
37
|
+
Changelog = "https://github.com/aura-way/pylolm/blob/main/CHANGELOG.md"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools]
|
|
40
|
+
package-dir = { "" = "src" }
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.packages.find]
|
|
43
|
+
where = ["src"]
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.package-data]
|
|
46
|
+
pylolm = ["py.typed", "lib/*.so", "native/*"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 100
|
|
50
|
+
target-version = "py39"
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = ["tests"]
|
pylolm-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""pylolm (LoLm): run GGUF language models on-device on Android, from Python.
|
|
2
|
+
|
|
3
|
+
An Android-friendly take on llama-cpp-python, built on llama.cpp and designed for
|
|
4
|
+
Kivy, Flet and Termux.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .errors import (
|
|
8
|
+
ContextOverflowError,
|
|
9
|
+
DownloadError,
|
|
10
|
+
GenerationError,
|
|
11
|
+
LibraryNotFoundError,
|
|
12
|
+
ModelLoadError,
|
|
13
|
+
PyLoLmError,
|
|
14
|
+
)
|
|
15
|
+
from .llama import Llama
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from importlib.metadata import PackageNotFoundError, version as _version
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
__version__ = _version("pylolm")
|
|
22
|
+
except PackageNotFoundError: # running from a source checkout
|
|
23
|
+
__version__ = "0.0.0+local"
|
|
24
|
+
except ImportError: # pragma: no cover
|
|
25
|
+
__version__ = "0.0.0+local"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"Llama",
|
|
29
|
+
"PyLoLmError",
|
|
30
|
+
"LibraryNotFoundError",
|
|
31
|
+
"ModelLoadError",
|
|
32
|
+
"ContextOverflowError",
|
|
33
|
+
"DownloadError",
|
|
34
|
+
"GenerationError",
|
|
35
|
+
"__version__",
|
|
36
|
+
]
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Command line: ``python -m pylolm {doctor,get-libs,build-native,chat}`` (also ``pylolm``)."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import platform
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from . import __version__, _lib
|
|
11
|
+
from .errors import DownloadError, PyLoLmError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def cmd_doctor(_args) -> int:
|
|
15
|
+
print("pylolm %s" % __version__)
|
|
16
|
+
print("python %s on %s (%s)" % (platform.python_version(), platform.system(), platform.machine()))
|
|
17
|
+
print("search order for native libraries:")
|
|
18
|
+
for d in _lib.candidate_dirs():
|
|
19
|
+
found = (d / _lib.SHIM_NAME).is_file()
|
|
20
|
+
print(" [%s] %s" % ("x" if found else " ", d))
|
|
21
|
+
try:
|
|
22
|
+
_lib.load()
|
|
23
|
+
except PyLoLmError as exc:
|
|
24
|
+
print("\nNOT READY: %s" % exc)
|
|
25
|
+
return 1
|
|
26
|
+
print("\nOK: native libraries loaded from %s" % _lib.loaded_dir())
|
|
27
|
+
return 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def cmd_get_libs(args) -> int:
|
|
31
|
+
from . import _download
|
|
32
|
+
|
|
33
|
+
pending = [False] # a progress line is on screen without a trailing newline
|
|
34
|
+
|
|
35
|
+
def progress(done, total):
|
|
36
|
+
pending[0] = True
|
|
37
|
+
mb = done / 1e6
|
|
38
|
+
text = " %.1f / %.1f MB" % (mb, total / 1e6) if total else " %.1f MB" % mb
|
|
39
|
+
print("\r" + text, end="", flush=True)
|
|
40
|
+
|
|
41
|
+
def log(message):
|
|
42
|
+
if pending[0]:
|
|
43
|
+
print()
|
|
44
|
+
pending[0] = False
|
|
45
|
+
print(message)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
_download.get_libs(
|
|
49
|
+
version=args.version,
|
|
50
|
+
repo=args.repo,
|
|
51
|
+
base_url=args.base_url,
|
|
52
|
+
force=args.force,
|
|
53
|
+
progress=progress,
|
|
54
|
+
log=log,
|
|
55
|
+
)
|
|
56
|
+
except DownloadError as exc:
|
|
57
|
+
if pending[0]:
|
|
58
|
+
print()
|
|
59
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
60
|
+
return 1
|
|
61
|
+
print("\nNext: pylolm doctor")
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def cmd_build_native(_args) -> int:
|
|
66
|
+
script = Path(__file__).resolve().parent / "native" / "build_termux.sh"
|
|
67
|
+
if not script.is_file():
|
|
68
|
+
print("build script not found: %s" % script, file=sys.stderr)
|
|
69
|
+
return 1
|
|
70
|
+
return subprocess.call(["bash", str(script)])
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cmd_chat(args) -> int:
|
|
74
|
+
from .llama import Llama
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
llm = Llama(args.model, n_ctx=args.n_ctx, n_threads=args.threads)
|
|
78
|
+
except (PyLoLmError, FileNotFoundError) as exc:
|
|
79
|
+
print("error: %s" % exc, file=sys.stderr)
|
|
80
|
+
return 1
|
|
81
|
+
|
|
82
|
+
history = []
|
|
83
|
+
if args.system:
|
|
84
|
+
history.append({"role": "system", "content": args.system})
|
|
85
|
+
print("pylolm chat - /reset clears history, /exit quits.\n")
|
|
86
|
+
|
|
87
|
+
while True:
|
|
88
|
+
try:
|
|
89
|
+
user = input("you> ").strip()
|
|
90
|
+
except (EOFError, KeyboardInterrupt):
|
|
91
|
+
print()
|
|
92
|
+
break
|
|
93
|
+
if not user:
|
|
94
|
+
continue
|
|
95
|
+
if user == "/exit":
|
|
96
|
+
break
|
|
97
|
+
if user == "/reset":
|
|
98
|
+
history = history[:1] if args.system else []
|
|
99
|
+
llm.reset()
|
|
100
|
+
print("(history cleared)")
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
history.append({"role": "user", "content": user})
|
|
104
|
+
reply = []
|
|
105
|
+
t0 = time.time()
|
|
106
|
+
n = 0
|
|
107
|
+
try:
|
|
108
|
+
print("bot> ", end="", flush=True)
|
|
109
|
+
for chunk in llm.create_chat_completion(
|
|
110
|
+
history, max_tokens=args.max_tokens, temperature=args.temp, stream=True
|
|
111
|
+
):
|
|
112
|
+
text = chunk["choices"][0]["delta"].get("content")
|
|
113
|
+
if text:
|
|
114
|
+
n += 1
|
|
115
|
+
reply.append(text)
|
|
116
|
+
print(text, end="", flush=True)
|
|
117
|
+
except KeyboardInterrupt:
|
|
118
|
+
llm.stop()
|
|
119
|
+
print("\n(interrupted)")
|
|
120
|
+
except PyLoLmError as exc:
|
|
121
|
+
print("\nerror: %s" % exc)
|
|
122
|
+
history.pop()
|
|
123
|
+
continue
|
|
124
|
+
dt = max(time.time() - t0, 1e-6)
|
|
125
|
+
print("\n[%d chunks, %.1f/s]\n" % (n, n / dt))
|
|
126
|
+
history.append({"role": "assistant", "content": "".join(reply)})
|
|
127
|
+
|
|
128
|
+
llm.close()
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def main(argv=None) -> int:
|
|
133
|
+
parser = argparse.ArgumentParser(prog="pylolm", description="Local LLMs on Android, from Python")
|
|
134
|
+
parser.add_argument("--version", action="version", version="pylolm " + __version__)
|
|
135
|
+
sub = parser.add_subparsers(dest="command")
|
|
136
|
+
|
|
137
|
+
sub.add_parser("doctor", help="check that the native libraries can be found and loaded")
|
|
138
|
+
sub.add_parser("build-native", help="build llama.cpp + the shim on this device (slow, Termux)")
|
|
139
|
+
|
|
140
|
+
get = sub.add_parser("get-libs", help="download the prebuilt native libraries (fast)")
|
|
141
|
+
get.add_argument("--version", default=None, help="release to download (default: this pylolm)")
|
|
142
|
+
get.add_argument("--repo", default=None, help="GitHub repo as owner/name")
|
|
143
|
+
get.add_argument("--base-url", default=None, help="download from this URL instead of GitHub")
|
|
144
|
+
get.add_argument("--force", action="store_true", help="replace existing libraries")
|
|
145
|
+
|
|
146
|
+
chat = sub.add_parser("chat", help="interactive chat in the terminal")
|
|
147
|
+
chat.add_argument("model", help="path to a .gguf file")
|
|
148
|
+
chat.add_argument("--n-ctx", type=int, default=2048)
|
|
149
|
+
chat.add_argument("--threads", type=int, default=None)
|
|
150
|
+
chat.add_argument("--system", default=None, help="system prompt")
|
|
151
|
+
chat.add_argument("--max-tokens", type=int, default=512)
|
|
152
|
+
chat.add_argument("--temp", type=float, default=0.7)
|
|
153
|
+
|
|
154
|
+
args = parser.parse_args(argv)
|
|
155
|
+
handlers = {
|
|
156
|
+
"doctor": cmd_doctor,
|
|
157
|
+
"get-libs": cmd_get_libs,
|
|
158
|
+
"build-native": cmd_build_native,
|
|
159
|
+
"chat": cmd_chat,
|
|
160
|
+
}
|
|
161
|
+
if args.command not in handlers:
|
|
162
|
+
parser.print_help()
|
|
163
|
+
return 0
|
|
164
|
+
return handlers[args.command](args)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
sys.exit(main())
|