dragon-cli 0.2.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.
- dragon_cli-0.2.0/LICENSE +21 -0
- dragon_cli-0.2.0/PKG-INFO +147 -0
- dragon_cli-0.2.0/README.md +123 -0
- dragon_cli-0.2.0/pyproject.toml +42 -0
- dragon_cli-0.2.0/setup.cfg +4 -0
- dragon_cli-0.2.0/src/dragon_cli/__init__.py +5 -0
- dragon_cli-0.2.0/src/dragon_cli/__main__.py +4 -0
- dragon_cli-0.2.0/src/dragon_cli/_cuda.py +42 -0
- dragon_cli-0.2.0/src/dragon_cli/app.py +123 -0
- dragon_cli-0.2.0/src/dragon_cli/banner.py +96 -0
- dragon_cli-0.2.0/src/dragon_cli/chat/__init__.py +4 -0
- dragon_cli-0.2.0/src/dragon_cli/chat/commands.py +175 -0
- dragon_cli-0.2.0/src/dragon_cli/chat/session.py +97 -0
- dragon_cli-0.2.0/src/dragon_cli/chat/streaming.py +18 -0
- dragon_cli-0.2.0/src/dragon_cli/cli.py +147 -0
- dragon_cli-0.2.0/src/dragon_cli/config.py +28 -0
- dragon_cli-0.2.0/src/dragon_cli/hardware.py +361 -0
- dragon_cli-0.2.0/src/dragon_cli/mcp/__init__.py +10 -0
- dragon_cli-0.2.0/src/dragon_cli/mcp/manager.py +148 -0
- dragon_cli-0.2.0/src/dragon_cli/mcp/tool_call.py +17 -0
- dragon_cli-0.2.0/src/dragon_cli/menu.py +1140 -0
- dragon_cli-0.2.0/src/dragon_cli/model_hub.py +252 -0
- dragon_cli-0.2.0/src/dragon_cli/models/__init__.py +11 -0
- dragon_cli-0.2.0/src/dragon_cli/models/catalog.py +205 -0
- dragon_cli-0.2.0/src/dragon_cli/models/loader.py +187 -0
- dragon_cli-0.2.0/src/dragon_cli/recommendation.py +295 -0
- dragon_cli-0.2.0/src/dragon_cli/runtime.py +252 -0
- dragon_cli-0.2.0/src/dragon_cli/server.py +144 -0
- dragon_cli-0.2.0/src/dragon_cli/setup_llm.py +197 -0
- dragon_cli-0.2.0/src/dragon_cli/storage/__init__.py +3 -0
- dragon_cli-0.2.0/src/dragon_cli/storage/conversations.py +134 -0
- dragon_cli-0.2.0/src/dragon_cli/storage/database.py +32 -0
- dragon_cli-0.2.0/src/dragon_cli/web.py +112 -0
- dragon_cli-0.2.0/src/dragon_cli.egg-info/PKG-INFO +147 -0
- dragon_cli-0.2.0/src/dragon_cli.egg-info/SOURCES.txt +37 -0
- dragon_cli-0.2.0/src/dragon_cli.egg-info/dependency_links.txt +1 -0
- dragon_cli-0.2.0/src/dragon_cli.egg-info/entry_points.txt +3 -0
- dragon_cli-0.2.0/src/dragon_cli.egg-info/requires.txt +11 -0
- dragon_cli-0.2.0/src/dragon_cli.egg-info/top_level.txt +1 -0
dragon_cli-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 JPEDROPS092
|
|
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,147 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dragon-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: CLI to chat with local GGUF LLMs, with MCP tool-calling and SQLite conversation history.
|
|
5
|
+
Author: JPEDROPS092
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://jpedrops092.github.io/dragon-cli-ai-local/
|
|
8
|
+
Project-URL: Repository, https://github.com/JPEDROPS092/dragon-cli-ai-local
|
|
9
|
+
Project-URL: Issues, https://github.com/JPEDROPS092/dragon-cli-ai-local/issues
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: rich
|
|
14
|
+
Requires-Dist: huggingface_hub
|
|
15
|
+
Requires-Dist: psutil
|
|
16
|
+
Requires-Dist: mcp
|
|
17
|
+
Requires-Dist: fastapi
|
|
18
|
+
Requires-Dist: starlette
|
|
19
|
+
Requires-Dist: uvicorn
|
|
20
|
+
Requires-Dist: fastapi
|
|
21
|
+
Provides-Extra: llm
|
|
22
|
+
Requires-Dist: llama-cpp-python; extra == "llm"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# Dragon CLI
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
[](LICENSE)
|
|
30
|
+
[](https://www.python.org/)
|
|
31
|
+
[](https://jpedrops092.github.io/dragon-cli-ai-local/)
|
|
32
|
+
[](https://github.com/JPEDROPS092/dragon-cli-ai-local/stargazers)
|
|
33
|
+
[](https://github.com/JPEDROPS092/dragon-cli-ai-local/issues)
|
|
34
|
+
|
|
35
|
+
Chat with local GGUF LLMs (100% CPU, via `llama-cpp-python`), with optional
|
|
36
|
+
MCP (Model Context Protocol) tool-calling and a SQLite-backed conversation
|
|
37
|
+
history.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install -e .
|
|
43
|
+
dragon-cli-install-llm
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This exposes a `dragon-cli` command. `llama-cpp-python` is intentionally kept
|
|
47
|
+
out of the core dependencies, since it ships as C++ source on PyPI for many
|
|
48
|
+
platforms — a plain `pip install` would try to compile it locally and fail
|
|
49
|
+
without a C/C++ toolchain (Visual Studio Build Tools on Windows, gcc on
|
|
50
|
+
Linux, Xcode command line tools on macOS).
|
|
51
|
+
|
|
52
|
+
`dragon-cli-install-llm` detects your OS/architecture (Windows, Linux, or
|
|
53
|
+
macOS) and installs a prebuilt CPU wheel instead, so a compiler usually isn't
|
|
54
|
+
needed at all. If no prebuilt wheel is available for your exact
|
|
55
|
+
platform/Python combination, it prints the toolchain to install and the
|
|
56
|
+
fallback command to run.
|
|
57
|
+
|
|
58
|
+
If you already have a working C/C++ toolchain and would rather build from
|
|
59
|
+
source yourself, skip the installer and use the extra instead:
|
|
60
|
+
`pip install -e ".[llm]"`.
|
|
61
|
+
|
|
62
|
+
You can also run it without installing:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
python dragon_cli.py
|
|
66
|
+
# or
|
|
67
|
+
python -m dragon_cli
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
dragon-cli # model menu + chat
|
|
74
|
+
dragon-cli --model 2 # skip the model menu
|
|
75
|
+
dragon-cli --list # just list the model catalog
|
|
76
|
+
dragon-cli --no-mcp # disable MCP connectors
|
|
77
|
+
dragon-cli --resume 3 # resume saved conversation #3
|
|
78
|
+
dragon-cli --list-conversations # list saved conversations and exit
|
|
79
|
+
dragon-cli --db path/to/other.db # use a different conversation database
|
|
80
|
+
dragon-cli --serve # serve the model over HTTP instead of chatting
|
|
81
|
+
dragon-cli --serve --port 8080 # custom port (default: 8000)
|
|
82
|
+
dragon-cli --serve --host 0.0.0.0 # expose beyond localhost (e.g. to your LAN) — opt-in only
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## HTTP server (`--serve`)
|
|
86
|
+
|
|
87
|
+
`dragon-cli --serve` loads a model and exposes it over a minimal
|
|
88
|
+
OpenAI-compatible HTTP API instead of starting the interactive chat. It's
|
|
89
|
+
built on FastAPI, so interactive docs come for free:
|
|
90
|
+
|
|
91
|
+
- `GET /` — landing page with usage examples
|
|
92
|
+
- `GET /docs` — Swagger UI
|
|
93
|
+
- `GET /redoc` — ReDoc
|
|
94
|
+
- `GET /openapi.json` — raw OpenAPI schema
|
|
95
|
+
- `GET /v1/models` — lists the currently loaded model
|
|
96
|
+
- `POST /v1/chat/completions` — same shape as OpenAI's endpoint, streaming
|
|
97
|
+
(`"stream": true`, server-sent events) and non-streaming both supported,
|
|
98
|
+
request body validated (422 with a clear error on malformed input)
|
|
99
|
+
|
|
100
|
+
Point any OpenAI-compatible client/SDK at `http://127.0.0.1:8000/v1` (adjust
|
|
101
|
+
host/port to match `--host`/`--port`) and it works without extra glue code.
|
|
102
|
+
This mode does **not** expose MCP tool-calling — it's the model only,
|
|
103
|
+
kept intentionally simple and predictable. It also binds to `127.0.0.1` by
|
|
104
|
+
default (only reachable from your own machine); pass `--host 0.0.0.0`
|
|
105
|
+
explicitly if you want it reachable from other devices on your network.
|
|
106
|
+
|
|
107
|
+
## In-chat commands
|
|
108
|
+
|
|
109
|
+
| Command | Description |
|
|
110
|
+
| -------------------------- | ----------------------------------- |
|
|
111
|
+
| `/help` | show available commands |
|
|
112
|
+
| `/models` | list the model catalog |
|
|
113
|
+
| `/tools` | list connected MCP tools |
|
|
114
|
+
| `/mcp` | show MCP server connection status |
|
|
115
|
+
| `/clear` | clear the current in-memory history |
|
|
116
|
+
| `/new [title]` | start a new conversation |
|
|
117
|
+
| `/list` | list saved conversations |
|
|
118
|
+
| `/resume <id>` | resume a saved conversation |
|
|
119
|
+
| `/rename <id> <title>` | rename a saved conversation |
|
|
120
|
+
| `/delete <id>` | delete a saved conversation |
|
|
121
|
+
| `/search <text>` | search saved messages for text |
|
|
122
|
+
| `/export <id> [md\|json]` | export a conversation to a file |
|
|
123
|
+
| `/exit`, `/quit` | end the program |
|
|
124
|
+
|
|
125
|
+
Every message is autosaved to SQLite (`dragon_cli.db` by default) as soon as
|
|
126
|
+
it's sent, so nothing is lost if the process crashes.
|
|
127
|
+
|
|
128
|
+
## MCP connectors
|
|
129
|
+
|
|
130
|
+
On first run, an example `mcp_servers.json` is created in the working
|
|
131
|
+
directory. Edit it to point at your MCP servers and restart to connect.
|
|
132
|
+
|
|
133
|
+
## Project layout
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
src/dragon_cli/
|
|
137
|
+
├── cli.py # argparse entry point
|
|
138
|
+
├── app.py # model selection + orchestration
|
|
139
|
+
├── setup_llm.py # cross-platform llama-cpp-python installer
|
|
140
|
+
├── server.py # OpenAI-compatible HTTP server (--serve)
|
|
141
|
+
├── banner.py # ASCII banner
|
|
142
|
+
├── config.py # shared paths/colors
|
|
143
|
+
├── models/ # model catalog, download, loading
|
|
144
|
+
├── mcp/ # MCP server manager + tool-call parsing
|
|
145
|
+
├── storage/ # SQLite conversation store
|
|
146
|
+
└── chat/ # chat session, streaming, slash commands
|
|
147
|
+
```
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Dragon CLI
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://www.python.org/)
|
|
7
|
+
[](https://jpedrops092.github.io/dragon-cli-ai-local/)
|
|
8
|
+
[](https://github.com/JPEDROPS092/dragon-cli-ai-local/stargazers)
|
|
9
|
+
[](https://github.com/JPEDROPS092/dragon-cli-ai-local/issues)
|
|
10
|
+
|
|
11
|
+
Chat with local GGUF LLMs (100% CPU, via `llama-cpp-python`), with optional
|
|
12
|
+
MCP (Model Context Protocol) tool-calling and a SQLite-backed conversation
|
|
13
|
+
history.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install -e .
|
|
19
|
+
dragon-cli-install-llm
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This exposes a `dragon-cli` command. `llama-cpp-python` is intentionally kept
|
|
23
|
+
out of the core dependencies, since it ships as C++ source on PyPI for many
|
|
24
|
+
platforms — a plain `pip install` would try to compile it locally and fail
|
|
25
|
+
without a C/C++ toolchain (Visual Studio Build Tools on Windows, gcc on
|
|
26
|
+
Linux, Xcode command line tools on macOS).
|
|
27
|
+
|
|
28
|
+
`dragon-cli-install-llm` detects your OS/architecture (Windows, Linux, or
|
|
29
|
+
macOS) and installs a prebuilt CPU wheel instead, so a compiler usually isn't
|
|
30
|
+
needed at all. If no prebuilt wheel is available for your exact
|
|
31
|
+
platform/Python combination, it prints the toolchain to install and the
|
|
32
|
+
fallback command to run.
|
|
33
|
+
|
|
34
|
+
If you already have a working C/C++ toolchain and would rather build from
|
|
35
|
+
source yourself, skip the installer and use the extra instead:
|
|
36
|
+
`pip install -e ".[llm]"`.
|
|
37
|
+
|
|
38
|
+
You can also run it without installing:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
python dragon_cli.py
|
|
42
|
+
# or
|
|
43
|
+
python -m dragon_cli
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
dragon-cli # model menu + chat
|
|
50
|
+
dragon-cli --model 2 # skip the model menu
|
|
51
|
+
dragon-cli --list # just list the model catalog
|
|
52
|
+
dragon-cli --no-mcp # disable MCP connectors
|
|
53
|
+
dragon-cli --resume 3 # resume saved conversation #3
|
|
54
|
+
dragon-cli --list-conversations # list saved conversations and exit
|
|
55
|
+
dragon-cli --db path/to/other.db # use a different conversation database
|
|
56
|
+
dragon-cli --serve # serve the model over HTTP instead of chatting
|
|
57
|
+
dragon-cli --serve --port 8080 # custom port (default: 8000)
|
|
58
|
+
dragon-cli --serve --host 0.0.0.0 # expose beyond localhost (e.g. to your LAN) — opt-in only
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## HTTP server (`--serve`)
|
|
62
|
+
|
|
63
|
+
`dragon-cli --serve` loads a model and exposes it over a minimal
|
|
64
|
+
OpenAI-compatible HTTP API instead of starting the interactive chat. It's
|
|
65
|
+
built on FastAPI, so interactive docs come for free:
|
|
66
|
+
|
|
67
|
+
- `GET /` — landing page with usage examples
|
|
68
|
+
- `GET /docs` — Swagger UI
|
|
69
|
+
- `GET /redoc` — ReDoc
|
|
70
|
+
- `GET /openapi.json` — raw OpenAPI schema
|
|
71
|
+
- `GET /v1/models` — lists the currently loaded model
|
|
72
|
+
- `POST /v1/chat/completions` — same shape as OpenAI's endpoint, streaming
|
|
73
|
+
(`"stream": true`, server-sent events) and non-streaming both supported,
|
|
74
|
+
request body validated (422 with a clear error on malformed input)
|
|
75
|
+
|
|
76
|
+
Point any OpenAI-compatible client/SDK at `http://127.0.0.1:8000/v1` (adjust
|
|
77
|
+
host/port to match `--host`/`--port`) and it works without extra glue code.
|
|
78
|
+
This mode does **not** expose MCP tool-calling — it's the model only,
|
|
79
|
+
kept intentionally simple and predictable. It also binds to `127.0.0.1` by
|
|
80
|
+
default (only reachable from your own machine); pass `--host 0.0.0.0`
|
|
81
|
+
explicitly if you want it reachable from other devices on your network.
|
|
82
|
+
|
|
83
|
+
## In-chat commands
|
|
84
|
+
|
|
85
|
+
| Command | Description |
|
|
86
|
+
| -------------------------- | ----------------------------------- |
|
|
87
|
+
| `/help` | show available commands |
|
|
88
|
+
| `/models` | list the model catalog |
|
|
89
|
+
| `/tools` | list connected MCP tools |
|
|
90
|
+
| `/mcp` | show MCP server connection status |
|
|
91
|
+
| `/clear` | clear the current in-memory history |
|
|
92
|
+
| `/new [title]` | start a new conversation |
|
|
93
|
+
| `/list` | list saved conversations |
|
|
94
|
+
| `/resume <id>` | resume a saved conversation |
|
|
95
|
+
| `/rename <id> <title>` | rename a saved conversation |
|
|
96
|
+
| `/delete <id>` | delete a saved conversation |
|
|
97
|
+
| `/search <text>` | search saved messages for text |
|
|
98
|
+
| `/export <id> [md\|json]` | export a conversation to a file |
|
|
99
|
+
| `/exit`, `/quit` | end the program |
|
|
100
|
+
|
|
101
|
+
Every message is autosaved to SQLite (`dragon_cli.db` by default) as soon as
|
|
102
|
+
it's sent, so nothing is lost if the process crashes.
|
|
103
|
+
|
|
104
|
+
## MCP connectors
|
|
105
|
+
|
|
106
|
+
On first run, an example `mcp_servers.json` is created in the working
|
|
107
|
+
directory. Edit it to point at your MCP servers and restart to connect.
|
|
108
|
+
|
|
109
|
+
## Project layout
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
src/dragon_cli/
|
|
113
|
+
├── cli.py # argparse entry point
|
|
114
|
+
├── app.py # model selection + orchestration
|
|
115
|
+
├── setup_llm.py # cross-platform llama-cpp-python installer
|
|
116
|
+
├── server.py # OpenAI-compatible HTTP server (--serve)
|
|
117
|
+
├── banner.py # ASCII banner
|
|
118
|
+
├── config.py # shared paths/colors
|
|
119
|
+
├── models/ # model catalog, download, loading
|
|
120
|
+
├── mcp/ # MCP server manager + tool-call parsing
|
|
121
|
+
├── storage/ # SQLite conversation store
|
|
122
|
+
└── chat/ # chat session, streaming, slash commands
|
|
123
|
+
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dragon-cli"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "CLI to chat with local GGUF LLMs, with MCP tool-calling and SQLite conversation history."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "JPEDROPS092" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"rich",
|
|
15
|
+
"huggingface_hub",
|
|
16
|
+
"psutil",
|
|
17
|
+
"mcp",
|
|
18
|
+
"fastapi",
|
|
19
|
+
"starlette",
|
|
20
|
+
"uvicorn",
|
|
21
|
+
"fastapi"
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
# llama-cpp-python ships as C++ source on PyPI for many platforms, so pulling
|
|
26
|
+
# it in as a hard dependency makes `pip install -e .` try to compile it and
|
|
27
|
+
# fail without a C/C++ toolchain. Install it separately with
|
|
28
|
+
# `dragon-cli-install-llm` (auto-detects OS/GPU, uses optimal prebuilt wheels),
|
|
29
|
+
# or via this extra if you already have a working compiler: pip install -e ".[llm]"
|
|
30
|
+
llm = ["llama-cpp-python"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://jpedrops092.github.io/dragon-cli-ai-local/"
|
|
34
|
+
Repository = "https://github.com/JPEDROPS092/dragon-cli-ai-local"
|
|
35
|
+
Issues = "https://github.com/JPEDROPS092/dragon-cli-ai-local/issues"
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
dragon-cli = "dragon_cli.cli:main"
|
|
39
|
+
dragon-cli-install-llm = "dragon_cli.setup_llm:main"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Preload CUDA runtime shared libraries shipped via pip wheels.
|
|
2
|
+
|
|
3
|
+
The GPU-enabled llama-cpp-python wheel links against libcudart/libcublas/
|
|
4
|
+
libnvrtc, which the pip ``nvidia-*-cu12`` packages install under
|
|
5
|
+
``site-packages/nvidia/*/lib`` (not on the system library path). Loading them
|
|
6
|
+
here by absolute path with ``RTLD_GLOBAL`` lets libllama.so resolve them
|
|
7
|
+
without LD_LIBRARY_PATH. No-op when the libs are already resolvable or absent
|
|
8
|
+
(e.g. the CPU-only wheel).
|
|
9
|
+
|
|
10
|
+
Kept dependency-free (stdlib only) so it can run at package import time
|
|
11
|
+
without risking circular imports.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import ctypes
|
|
15
|
+
import glob
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
_CUDA_SONAMES = (
|
|
20
|
+
"libcudart.so.12",
|
|
21
|
+
"libcublas.so.12",
|
|
22
|
+
"libcublasLt.so.12",
|
|
23
|
+
"libnvrtc.so.12",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def preload_cuda_runtime() -> None:
|
|
28
|
+
loaded: set[str] = set()
|
|
29
|
+
for entry in sys.path:
|
|
30
|
+
nvidia_dir = os.path.join(entry, "nvidia")
|
|
31
|
+
if not entry or not os.path.isdir(nvidia_dir):
|
|
32
|
+
continue
|
|
33
|
+
for soname in _CUDA_SONAMES:
|
|
34
|
+
if soname in loaded:
|
|
35
|
+
continue
|
|
36
|
+
for match in glob.glob(os.path.join(nvidia_dir, "*", "lib", soname)):
|
|
37
|
+
try:
|
|
38
|
+
ctypes.CDLL(match, mode=ctypes.RTLD_GLOBAL)
|
|
39
|
+
loaded.add(soname)
|
|
40
|
+
break
|
|
41
|
+
except OSError:
|
|
42
|
+
continue
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Async orchestration: model selection, MCP startup, and the chat session."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .chat import ChatSession, chat_loop
|
|
6
|
+
from .config import C_RED, C_RESET
|
|
7
|
+
from .mcp import MCPManager
|
|
8
|
+
from .models import MODEL_CATALOG, available_ram_gb, download_model, list_catalog, load_llm
|
|
9
|
+
from .storage import ConversationStore
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _gpu_layers_arg(args) -> int | None:
|
|
13
|
+
"""Translate ``--n-gpu-layers`` into a ``load_llm`` override.
|
|
14
|
+
|
|
15
|
+
The CLI default is -1 (auto). Any value >= 0 is an explicit override
|
|
16
|
+
(0 = CPU only, >0 = exact layer count). None means "let the runtime
|
|
17
|
+
auto-decide".
|
|
18
|
+
"""
|
|
19
|
+
n = getattr(args, "n_gpu_layers", -1)
|
|
20
|
+
return n if n is not None and n >= 0 else None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def choose_model(args) -> dict | None:
|
|
24
|
+
# Handle --auto flag for automatic model selection
|
|
25
|
+
if getattr(args, 'auto', False):
|
|
26
|
+
from .recommendation import ModelRecommender
|
|
27
|
+
recommender = ModelRecommender()
|
|
28
|
+
best_model = recommender.recommend_best(priority=getattr(args, 'priority', 'balanced'))
|
|
29
|
+
|
|
30
|
+
if best_model:
|
|
31
|
+
print(f"\n{C_RESET}Auto-selected model based on your hardware:")
|
|
32
|
+
print(f" → {best_model['name']}")
|
|
33
|
+
reason = recommender.get_recommendation_reason(best_model)
|
|
34
|
+
print(f" {C_DIM}Why: {reason}{C_RESET}\n")
|
|
35
|
+
return best_model
|
|
36
|
+
else:
|
|
37
|
+
print(f"{C_RED}[error]{C_RESET} No compatible models found for your hardware.")
|
|
38
|
+
# Fall back to manual selection
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
# Skip the plain catalog print when a model was already chosen upstream
|
|
42
|
+
# (e.g. by the rich interactive menu), so the user doesn't see it twice.
|
|
43
|
+
if not args.model:
|
|
44
|
+
list_catalog()
|
|
45
|
+
if args.model:
|
|
46
|
+
choice = args.model
|
|
47
|
+
else:
|
|
48
|
+
try:
|
|
49
|
+
choice = int(input("\nChoose a model number: ").strip())
|
|
50
|
+
except (ValueError, EOFError, KeyboardInterrupt):
|
|
51
|
+
print("Invalid choice.")
|
|
52
|
+
return None
|
|
53
|
+
if not (1 <= choice <= len(MODEL_CATALOG)):
|
|
54
|
+
print("Number not in the catalog.")
|
|
55
|
+
return None
|
|
56
|
+
model = MODEL_CATALOG[choice - 1]
|
|
57
|
+
ram = available_ram_gb()
|
|
58
|
+
if 0 < ram < model["ram_min_gb"]:
|
|
59
|
+
ok = input(f"{C_RED}[warning]{C_RESET} Model needs ~{model['ram_min_gb']} GB, "
|
|
60
|
+
f"you have {ram:.1f} GB free. Continue? (y/N) ").strip().lower()
|
|
61
|
+
if ok != "y":
|
|
62
|
+
return None
|
|
63
|
+
return model
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def print_conversations(store: ConversationStore) -> None:
|
|
67
|
+
conversations = store.list()
|
|
68
|
+
if not conversations:
|
|
69
|
+
print("No conversations yet.")
|
|
70
|
+
return
|
|
71
|
+
for c in conversations:
|
|
72
|
+
print(f" #{c.id} {c.title} ({c.message_count} msgs, updated {c.updated_at})")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def run(args) -> None:
|
|
76
|
+
if args.serve:
|
|
77
|
+
model = choose_model(args)
|
|
78
|
+
if not model:
|
|
79
|
+
return
|
|
80
|
+
path = download_model(model)
|
|
81
|
+
llm = load_llm(path, model["ctx"], model_info=model,
|
|
82
|
+
gpu_layers=_gpu_layers_arg(args),
|
|
83
|
+
force_cpu=getattr(args, "cpu", False),
|
|
84
|
+
force_gpu=getattr(args, "gpu", False))
|
|
85
|
+
from .server import run_server
|
|
86
|
+
await run_server(llm, model["name"], args.host, args.port)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
store = ConversationStore(Path(args.db))
|
|
90
|
+
|
|
91
|
+
if args.list_conversations:
|
|
92
|
+
print_conversations(store)
|
|
93
|
+
store.close()
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
model = choose_model(args)
|
|
97
|
+
if not model:
|
|
98
|
+
store.close()
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
path = download_model(model)
|
|
102
|
+
llm = load_llm(path, model["ctx"], model_info=model,
|
|
103
|
+
gpu_layers=_gpu_layers_arg(args),
|
|
104
|
+
force_cpu=getattr(args, "cpu", False),
|
|
105
|
+
force_gpu=getattr(args, "gpu", False))
|
|
106
|
+
|
|
107
|
+
mcp = MCPManager()
|
|
108
|
+
if not args.no_mcp:
|
|
109
|
+
await mcp.start()
|
|
110
|
+
|
|
111
|
+
session = ChatSession(llm, mcp, store, model["name"])
|
|
112
|
+
if args.resume is not None:
|
|
113
|
+
if not session.resume(args.resume):
|
|
114
|
+
print(f"{C_RED}[error]{C_RESET} No conversation with id {args.resume}, starting a new one.")
|
|
115
|
+
session.start_new()
|
|
116
|
+
else:
|
|
117
|
+
session.start_new()
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
await chat_loop(session)
|
|
121
|
+
finally:
|
|
122
|
+
await mcp.stop()
|
|
123
|
+
store.close()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""ASCII dragon banner shown at startup."""
|
|
2
|
+
|
|
3
|
+
BANNER = r'''
|
|
4
|
+
_,_
|
|
5
|
+
/7/Y/^\
|
|
6
|
+
vuVV|C)| __ _
|
|
7
|
+
\|^ / .' Y '>,
|
|
8
|
+
)| \) / _ _ \
|
|
9
|
+
//)|\\ )(_) (_)(|}
|
|
10
|
+
/ ^| \ \ { 4A } /
|
|
11
|
+
//^| || \\ \uLuJJ/\l
|
|
12
|
+
>//||| |\\\| |3 p)/
|
|
13
|
+
| """"" 7/>l__ _____ ____ /nnm_n//
|
|
14
|
+
L>_ _-< 7/|_-__,__-)\,__)(". \_>-<_/D
|
|
15
|
+
)D" Y "c) 9) //V \_"-._.__G<D __
|
|
16
|
+
\ || . _/ ( \ "-._"> G<D-_.-"_=)_
|
|
17
|
+
) __ / / " _/.__ />_c__.-__<"/ ( \
|
|
18
|
+
| | | |(| < "-._"> _.G_.___)\ \7\
|
|
19
|
+
\"=" // | (,"-.__.|\ \<.__.-" ) \ \
|
|
20
|
+
'---' | |,"-.__"| \!"-.__.-".\ \ \
|
|
21
|
+
|_;._/ (_"-.__"'\ \"-.__.-".| \_\
|
|
22
|
+
)(" V \"-.__"'|\ \-.__.-".) \ \
|
|
23
|
+
( "-.__'"\_\ \.__.-"./ \ l
|
|
24
|
+
) ".__"">>G\ \__.-"> V )
|
|
25
|
+
AI PROVIDERS "" G<\ \_.-" ./7
|
|
26
|
+
G<\ \ ///
|
|
27
|
+
___ G<D\ \ /7^
|
|
28
|
+
/".-_"-"G"<\ \__ ///))
|
|
29
|
+
||,-_ " '.\_"- /// (\
|
|
30
|
+
| (_) _ "/ __7 |X/ V
|
|
31
|
+
/\__""-/ "\ _""c_"GBV
|
|
32
|
+
_)( ""' "--.c__>D'
|
|
33
|
+
/ / )E_>"
|
|
34
|
+
_/ ( | \()
|
|
35
|
+
/ \ / | |
|
|
36
|
+
/\\| | |
|
|
37
|
+
/ '(( | |
|
|
38
|
+
/ / )\ \ |
|
|
39
|
+
/ y \y |Y |
|
|
40
|
+
/ / ( | |
|
|
41
|
+
L ." | | /
|
|
42
|
+
| \( | |
|
|
43
|
+
\_| | |
|
|
44
|
+
| \ { "|
|
|
45
|
+
| || | |
|
|
46
|
+
|x|| \_,/
|
|
47
|
+
} || / \'
|
|
48
|
+
| || |_/
|
|
49
|
+
| (| | }\
|
|
50
|
+
| || } ||
|
|
51
|
+
| || | ||
|
|
52
|
+
| || |\||
|
|
53
|
+
/ || | ||
|
|
54
|
+
| || ( |!
|
|
55
|
+
| |/ ) ||
|
|
56
|
+
_/ \ | }|
|
|
57
|
+
_.-"_ ( )| jjs ! ||
|
|
58
|
+
c_"-_-"_ ) | ||
|
|
59
|
+
c,-_-'_--"" { ||
|
|
60
|
+
"C(_/" \ /|
|
|
61
|
+
(! )
|
|
62
|
+
/| \
|
|
63
|
+
/ |(
|
|
64
|
+
/7||\\
|
|
65
|
+
()U cUu"
|
|
66
|
+
'''
|
|
67
|
+
|
|
68
|
+
SUBTITLE = " DRAGON CLI — local LLM + MCP | /help for commands"
|
|
69
|
+
|
|
70
|
+
# Compact mascot icon shown as a splash on the interactive menu. Raw string so
|
|
71
|
+
# the backslashes in the art are preserved verbatim.
|
|
72
|
+
ICON = r""",--.
|
|
73
|
+
{ }
|
|
74
|
+
K, }
|
|
75
|
+
/ `Y`
|
|
76
|
+
_ / /
|
|
77
|
+
{_'-K.__/
|
|
78
|
+
`/-.__L._
|
|
79
|
+
/ ' /`\_}
|
|
80
|
+
/ ' / -AI PROVIDERS-
|
|
81
|
+
____ / ' /
|
|
82
|
+
,-'~~~~ ~~/ ' /_
|
|
83
|
+
,' ``~~~%%',
|
|
84
|
+
( % Y
|
|
85
|
+
{ %% I
|
|
86
|
+
{ - % `.
|
|
87
|
+
| ', % )
|
|
88
|
+
| | ,..__ __. Y
|
|
89
|
+
| .,_./ Y ' / ^Y J )|
|
|
90
|
+
\ |' / | | ||
|
|
91
|
+
\ L_/ . _ (_,.'(
|
|
92
|
+
\, , ^^""' / | )
|
|
93
|
+
\_ \ /,L] /
|
|
94
|
+
'-_`-, ` ` ./`
|
|
95
|
+
`-(_ )
|
|
96
|
+
^^\..___,.--`"""
|