dragon-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dragon_cli/__init__.py +5 -0
- dragon_cli/__main__.py +4 -0
- dragon_cli/_cuda.py +42 -0
- dragon_cli/app.py +123 -0
- dragon_cli/banner.py +96 -0
- dragon_cli/chat/__init__.py +4 -0
- dragon_cli/chat/commands.py +175 -0
- dragon_cli/chat/session.py +97 -0
- dragon_cli/chat/streaming.py +18 -0
- dragon_cli/cli.py +147 -0
- dragon_cli/config.py +28 -0
- dragon_cli/hardware.py +361 -0
- dragon_cli/mcp/__init__.py +10 -0
- dragon_cli/mcp/manager.py +148 -0
- dragon_cli/mcp/tool_call.py +17 -0
- dragon_cli/menu.py +1140 -0
- dragon_cli/model_hub.py +252 -0
- dragon_cli/models/__init__.py +11 -0
- dragon_cli/models/catalog.py +205 -0
- dragon_cli/models/loader.py +187 -0
- dragon_cli/recommendation.py +295 -0
- dragon_cli/runtime.py +252 -0
- dragon_cli/server.py +144 -0
- dragon_cli/setup_llm.py +197 -0
- dragon_cli/storage/__init__.py +3 -0
- dragon_cli/storage/conversations.py +134 -0
- dragon_cli/storage/database.py +32 -0
- dragon_cli/web.py +112 -0
- dragon_cli-0.2.0.dist-info/METADATA +147 -0
- dragon_cli-0.2.0.dist-info/RECORD +34 -0
- dragon_cli-0.2.0.dist-info/WHEEL +5 -0
- dragon_cli-0.2.0.dist-info/entry_points.txt +3 -0
- dragon_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- dragon_cli-0.2.0.dist-info/top_level.txt +1 -0
dragon_cli/cli.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Dragon CLI — chat with local LLMs (GGUF) + MCP connectors.
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
* ASCII dragon banner at startup
|
|
6
|
+
* Downloads and stores GGUF models locally (./models)
|
|
7
|
+
* Interactive streaming chat, 100% on CPU
|
|
8
|
+
* MCP (Model Context Protocol) connectors via mcp_servers.json
|
|
9
|
+
* Tool-calling: the model can call tools exposed by MCP servers
|
|
10
|
+
* SQLite-backed conversation history: list/new/resume/rename/delete/search/export
|
|
11
|
+
* --serve: OpenAI-compatible HTTP endpoint for the loaded model
|
|
12
|
+
* --profile: Show hardware profile
|
|
13
|
+
* --recommend: Show model recommendations
|
|
14
|
+
* --auto: Automatically select best model for your hardware
|
|
15
|
+
|
|
16
|
+
Install (in your venv):
|
|
17
|
+
pip install -e .
|
|
18
|
+
dragon-cli-install-llm (detects your OS/GPU, installs optimal llama-cpp-python wheel)
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
dragon-cli # model menu + chat
|
|
22
|
+
dragon-cli --model 2 # skip the model menu
|
|
23
|
+
dragon-cli --list # just list models
|
|
24
|
+
dragon-cli --no-mcp # disable MCP
|
|
25
|
+
dragon-cli --resume 3 # resume saved conversation #3
|
|
26
|
+
dragon-cli --list-conversations # list saved conversations and exit
|
|
27
|
+
dragon-cli --serve --port 8000 # serve the model over HTTP instead of chatting
|
|
28
|
+
dragon-cli --profile # show hardware profile
|
|
29
|
+
dragon-cli --recommend # show model recommendations
|
|
30
|
+
dragon-cli --auto # auto-select best model
|
|
31
|
+
|
|
32
|
+
In-chat commands:
|
|
33
|
+
/help /models /tools /mcp /clear
|
|
34
|
+
/new /list /resume /rename /delete
|
|
35
|
+
/search /export /exit
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import argparse
|
|
39
|
+
import asyncio
|
|
40
|
+
import os
|
|
41
|
+
import sys
|
|
42
|
+
|
|
43
|
+
from . import app
|
|
44
|
+
from .banner import BANNER, SUBTITLE
|
|
45
|
+
from .config import C_CYAN, C_RESET, C_YELLOW, DB_FILE
|
|
46
|
+
from .models import list_catalog
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
50
|
+
parser = argparse.ArgumentParser(description="Dragon CLI — local LLM + MCP")
|
|
51
|
+
parser.add_argument("--list", action="store_true", help="list the model catalog and exit")
|
|
52
|
+
parser.add_argument("--model", type=int, help="model number from the catalog")
|
|
53
|
+
parser.add_argument("--no-mcp", action="store_true", help="don't connect to MCP servers")
|
|
54
|
+
parser.add_argument("--db", default=str(DB_FILE), help="path to the SQLite conversation database")
|
|
55
|
+
parser.add_argument("--resume", type=int, default=None, help="resume a saved conversation by id")
|
|
56
|
+
parser.add_argument("--list-conversations", action="store_true", help="list saved conversations and exit")
|
|
57
|
+
parser.add_argument("--serve", action="store_true",
|
|
58
|
+
help="serve the model over an OpenAI-compatible HTTP endpoint instead of chatting")
|
|
59
|
+
parser.add_argument("--port", type=int, default=8000, help="port for --serve (default: 8000)")
|
|
60
|
+
parser.add_argument("--host", default="127.0.0.1",
|
|
61
|
+
help="bind address for --serve (default: 127.0.0.1, localhost only)")
|
|
62
|
+
parser.add_argument("--profile", action="store_true",
|
|
63
|
+
help="show hardware profile and exit")
|
|
64
|
+
parser.add_argument("--recommend", action="store_true",
|
|
65
|
+
help="show model recommendations for your hardware and exit")
|
|
66
|
+
parser.add_argument("--priority", choices=["speed", "quality", "balanced", "memory"],
|
|
67
|
+
default="balanced", help="optimization priority for --recommend or --auto")
|
|
68
|
+
parser.add_argument("--auto", action="store_true",
|
|
69
|
+
help="automatically select the best model for your hardware")
|
|
70
|
+
|
|
71
|
+
# --- GPU / execution backend control ---------------------------------
|
|
72
|
+
gpu_group = parser.add_mutually_exclusive_group()
|
|
73
|
+
gpu_group.add_argument("--gpu", action="store_true",
|
|
74
|
+
help="force GPU acceleration (offload all layers) when a GPU is present")
|
|
75
|
+
gpu_group.add_argument("--cpu", action="store_true",
|
|
76
|
+
help="force CPU-only mode (ignore any GPU)")
|
|
77
|
+
parser.add_argument("--n-gpu-layers", "--ngl", dest="n_gpu_layers", type=int, default=-1,
|
|
78
|
+
help="number of model layers to offload to the GPU "
|
|
79
|
+
"(-1 = auto from hardware, 0 = CPU only, >0 = explicit count)")
|
|
80
|
+
return parser
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _has_action_flag(args) -> bool:
|
|
84
|
+
"""True if a CLI flag targets a specific action (skip the fancy menu)."""
|
|
85
|
+
return any(
|
|
86
|
+
getattr(args, name, False)
|
|
87
|
+
for name in ("list", "profile", "recommend", "serve",
|
|
88
|
+
"list_conversations", "auto", "model", "resume")
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main() -> None:
|
|
93
|
+
if os.name == "nt":
|
|
94
|
+
os.system("") # enable ANSI colors in the Windows console
|
|
95
|
+
|
|
96
|
+
args = build_parser().parse_args()
|
|
97
|
+
|
|
98
|
+
# Single-shot, print-and-exit flags (plain banner + ANSI output).
|
|
99
|
+
if args.profile:
|
|
100
|
+
print(C_CYAN + BANNER + C_RESET)
|
|
101
|
+
from .hardware import print_hardware_profile
|
|
102
|
+
print_hardware_profile()
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
if args.recommend:
|
|
106
|
+
from .recommendation import recommend_model_for_hardware
|
|
107
|
+
recommend_model_for_hardware(priority=args.priority, show_all=True)
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
if args.list:
|
|
111
|
+
list_catalog()
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# Default (no action flag): launch the fancy rich-based interactive menu.
|
|
115
|
+
# The menu does its own per-action dependency checks, so it shows even if
|
|
116
|
+
# llama-cpp-python isn't installed yet.
|
|
117
|
+
if not _has_action_flag(args):
|
|
118
|
+
from .menu import run as run_menu
|
|
119
|
+
run_menu(args)
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
print(C_CYAN + BANNER + C_RESET)
|
|
123
|
+
print(C_YELLOW + SUBTITLE + C_RESET + "\n")
|
|
124
|
+
|
|
125
|
+
required = [("llama_cpp", "llama-cpp-python"), ("huggingface_hub", "huggingface_hub")]
|
|
126
|
+
if args.serve:
|
|
127
|
+
required += [("fastapi", "fastapi"), ("starlette", "starlette"), ("uvicorn", "uvicorn")]
|
|
128
|
+
missing = []
|
|
129
|
+
for module, pip_name in required:
|
|
130
|
+
try:
|
|
131
|
+
__import__(module)
|
|
132
|
+
except ImportError:
|
|
133
|
+
missing.append(pip_name)
|
|
134
|
+
if missing and not args.list_conversations:
|
|
135
|
+
if "llama-cpp-python" in missing:
|
|
136
|
+
print("Missing dependency: llama-cpp-python.")
|
|
137
|
+
print("Run: dragon-cli-install-llm (detects your OS/GPU and installs the optimal wheel)")
|
|
138
|
+
missing.remove("llama-cpp-python")
|
|
139
|
+
if missing:
|
|
140
|
+
print(f"Install the remaining dependencies: pip install {' '.join(missing)}")
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
|
|
143
|
+
asyncio.run(app.run(args))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
main()
|
dragon_cli/config.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Paths, colors, and other shared constants for Dragon CLI."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
MODELS_DIR = Path("models")
|
|
6
|
+
MCP_CONFIG_FILE = Path("mcp_servers.json")
|
|
7
|
+
DB_FILE = Path("dragon_cli.db")
|
|
8
|
+
|
|
9
|
+
# Simple ANSI colors (work in modern Windows Terminal / PowerShell).
|
|
10
|
+
C_RESET = "\033[0m"
|
|
11
|
+
C_CYAN = "\033[36m"
|
|
12
|
+
C_YELLOW = "\033[33m"
|
|
13
|
+
C_GREEN = "\033[32m"
|
|
14
|
+
C_RED = "\033[31m"
|
|
15
|
+
C_DIM = "\033[2m"
|
|
16
|
+
|
|
17
|
+
EXAMPLE_MCP_CONFIG = {
|
|
18
|
+
"mcpServers": {
|
|
19
|
+
"filesystem": {
|
|
20
|
+
"command": "npx",
|
|
21
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
|
|
22
|
+
},
|
|
23
|
+
"fetch": {
|
|
24
|
+
"command": "uvx",
|
|
25
|
+
"args": ["--with", "mcp<1.0", "mcp-server-fetch"],
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
}
|
dragon_cli/hardware.py
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""Hardware detection module for Dragon CLI.
|
|
2
|
+
|
|
3
|
+
Detects CPU, RAM, GPU, storage, and operating system to calculate
|
|
4
|
+
execution capacity for LLM models.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import platform
|
|
8
|
+
import shutil
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_os_info() -> str:
|
|
13
|
+
"""Return operating system information."""
|
|
14
|
+
system = platform.system()
|
|
15
|
+
release = platform.release()
|
|
16
|
+
version = platform.version()
|
|
17
|
+
machine = platform.machine()
|
|
18
|
+
|
|
19
|
+
if system == "Windows":
|
|
20
|
+
# Get more detailed Windows version
|
|
21
|
+
win_ver = platform.win32_ver()
|
|
22
|
+
return f"{system} {release} ({win_ver[0]}) {machine}"
|
|
23
|
+
elif system == "Darwin":
|
|
24
|
+
return f"macOS {release} ({machine})"
|
|
25
|
+
else:
|
|
26
|
+
# Linux or other
|
|
27
|
+
try:
|
|
28
|
+
with open("/etc/os-release", "r") as f:
|
|
29
|
+
for line in f:
|
|
30
|
+
if line.startswith("PRETTY_NAME="):
|
|
31
|
+
pretty_name = line.strip().split("=", 1)[1].strip('"')
|
|
32
|
+
return f"{pretty_name} {machine}"
|
|
33
|
+
except FileNotFoundError:
|
|
34
|
+
pass
|
|
35
|
+
return f"{system} {release} ({machine})"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_cpu_info() -> Dict[str, Any]:
|
|
39
|
+
"""Return CPU information."""
|
|
40
|
+
import os
|
|
41
|
+
|
|
42
|
+
cpu_count = os.cpu_count() or 0
|
|
43
|
+
# Physical count estimation (usually half of logical for hyperthreading)
|
|
44
|
+
cpu_count_physical = max(1, cpu_count // 2) if cpu_count > 0 else 0
|
|
45
|
+
|
|
46
|
+
# Try to get CPU model/name
|
|
47
|
+
cpu_model = "Unknown"
|
|
48
|
+
|
|
49
|
+
if platform.system() == "Windows":
|
|
50
|
+
try:
|
|
51
|
+
import ctypes
|
|
52
|
+
# Use wmi or registry for Windows CPU info
|
|
53
|
+
import subprocess
|
|
54
|
+
result = subprocess.run(
|
|
55
|
+
["wmic", "cpu", "get", "name"],
|
|
56
|
+
capture_output=True, text=True, timeout=5
|
|
57
|
+
)
|
|
58
|
+
lines = result.stdout.strip().split("\n")
|
|
59
|
+
if len(lines) >= 2:
|
|
60
|
+
cpu_model = lines[1].strip()
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
elif platform.system() == "Darwin":
|
|
64
|
+
try:
|
|
65
|
+
import subprocess
|
|
66
|
+
result = subprocess.run(
|
|
67
|
+
["sysctl", "-n", "machdep.cpu.brand_string"],
|
|
68
|
+
capture_output=True, text=True, timeout=5
|
|
69
|
+
)
|
|
70
|
+
if result.returncode == 0:
|
|
71
|
+
cpu_model = result.stdout.strip()
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
else:
|
|
75
|
+
# Linux
|
|
76
|
+
try:
|
|
77
|
+
with open("/proc/cpuinfo", "r") as f:
|
|
78
|
+
for line in f:
|
|
79
|
+
if line.startswith("model name"):
|
|
80
|
+
cpu_model = line.split(":", 1)[1].strip()
|
|
81
|
+
break
|
|
82
|
+
except FileNotFoundError:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
"cpu_count": cpu_count,
|
|
87
|
+
"cpu_count_physical": cpu_count_physical,
|
|
88
|
+
"cpu_model": cpu_model,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_ram_info() -> Dict[str, float]:
|
|
93
|
+
"""Return RAM information in GB."""
|
|
94
|
+
try:
|
|
95
|
+
import psutil
|
|
96
|
+
vm = psutil.virtual_memory()
|
|
97
|
+
ram_total_gb = vm.total / (1024 ** 3)
|
|
98
|
+
ram_available_gb = vm.available / (1024 ** 3)
|
|
99
|
+
ram_used_gb = vm.used / (1024 ** 3)
|
|
100
|
+
ram_percent = vm.percent
|
|
101
|
+
except ImportError:
|
|
102
|
+
# Fallback without psutil
|
|
103
|
+
if platform.system() == "Linux":
|
|
104
|
+
try:
|
|
105
|
+
with open("/proc/meminfo", "r") as f:
|
|
106
|
+
mem_info = {}
|
|
107
|
+
for line in f:
|
|
108
|
+
parts = line.split(":")
|
|
109
|
+
if len(parts) == 2:
|
|
110
|
+
key = parts[0].strip()
|
|
111
|
+
value_str = parts[1].strip().split()[0]
|
|
112
|
+
try:
|
|
113
|
+
mem_info[key] = int(value_str)
|
|
114
|
+
except ValueError:
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
# Values are in kB
|
|
118
|
+
ram_total_gb = mem_info.get("MemTotal", 0) / (1024 ** 2)
|
|
119
|
+
ram_free_gb = mem_info.get("MemFree", 0) / (1024 ** 2)
|
|
120
|
+
ram_available_gb = mem_info.get("MemAvailable", ram_free_gb) / (1024 ** 2)
|
|
121
|
+
ram_used_gb = ram_total_gb - ram_free_gb
|
|
122
|
+
ram_percent = (ram_used_gb / ram_total_gb * 100) if ram_total_gb > 0 else 0
|
|
123
|
+
except FileNotFoundError:
|
|
124
|
+
ram_total_gb = -1
|
|
125
|
+
ram_available_gb = -1
|
|
126
|
+
ram_used_gb = -1
|
|
127
|
+
ram_percent = -1
|
|
128
|
+
else:
|
|
129
|
+
ram_total_gb = -1
|
|
130
|
+
ram_available_gb = -1
|
|
131
|
+
ram_used_gb = -1
|
|
132
|
+
ram_percent = -1
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
"ram_total_gb": round(ram_total_gb, 2),
|
|
136
|
+
"ram_available_gb": round(ram_available_gb, 2),
|
|
137
|
+
"ram_used_gb": round(ram_used_gb, 2),
|
|
138
|
+
"ram_percent": round(ram_percent, 2),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_gpu_info() -> Dict[str, Any]:
|
|
143
|
+
"""Return GPU information with graceful fallback."""
|
|
144
|
+
gpu_name = "None detected"
|
|
145
|
+
gpu_vram_gb = 0.0
|
|
146
|
+
gpu_available = False
|
|
147
|
+
|
|
148
|
+
# Try NVIDIA CUDA
|
|
149
|
+
try:
|
|
150
|
+
import ctypes
|
|
151
|
+
# Try to load CUDA library
|
|
152
|
+
cuda_lib_names = ["cuda", "nvcuda", "libcuda.so", "libcuda.so.1"]
|
|
153
|
+
cuda_lib = None
|
|
154
|
+
|
|
155
|
+
for lib_name in cuda_lib_names:
|
|
156
|
+
try:
|
|
157
|
+
cuda_lib = ctypes.CDLL(lib_name)
|
|
158
|
+
break
|
|
159
|
+
except OSError:
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
if cuda_lib is not None:
|
|
163
|
+
# The driver API must be initialized before any query, otherwise
|
|
164
|
+
# cuDeviceGetCount returns CUDA_ERROR_NOT_INITIALIZED (3) and the
|
|
165
|
+
# GPU is wrongly reported as absent.
|
|
166
|
+
cuda_lib.cuInit(0)
|
|
167
|
+
|
|
168
|
+
# Check CUDA device count
|
|
169
|
+
device_count = ctypes.c_int()
|
|
170
|
+
result = cuda_lib.cuDeviceGetCount(ctypes.byref(device_count))
|
|
171
|
+
|
|
172
|
+
if result == 0 and device_count.value > 0:
|
|
173
|
+
gpu_available = True
|
|
174
|
+
# Get first device name
|
|
175
|
+
device = ctypes.c_int()
|
|
176
|
+
cuda_lib.cuDeviceGet(ctypes.byref(device), 0)
|
|
177
|
+
|
|
178
|
+
name_buffer = ctypes.create_string_buffer(256)
|
|
179
|
+
cuda_lib.cuDeviceGetName(name_buffer, 256, device)
|
|
180
|
+
gpu_name = name_buffer.value.decode("utf-8")
|
|
181
|
+
|
|
182
|
+
# Total VRAM via the driver API. cuMemGetInfo requires an
|
|
183
|
+
# active CUDA context, but cuDeviceTotalMem_v2 does not — so it
|
|
184
|
+
# works without creating/pinning a context (fall back to v1).
|
|
185
|
+
total_mem = ctypes.c_size_t()
|
|
186
|
+
for fn_name in ("cuDeviceTotalMem_v2", "cuDeviceTotalMem"):
|
|
187
|
+
fn = getattr(cuda_lib, fn_name, None)
|
|
188
|
+
if fn is not None and fn(ctypes.byref(total_mem), device) == 0:
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
gpu_vram_gb = total_mem.value / (1024 ** 3)
|
|
192
|
+
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
# If no CUDA, try pytorch for GPU detection
|
|
197
|
+
if not gpu_available:
|
|
198
|
+
try:
|
|
199
|
+
import torch
|
|
200
|
+
if torch.cuda.is_available():
|
|
201
|
+
gpu_available = True
|
|
202
|
+
gpu_name = torch.cuda.get_device_name(0)
|
|
203
|
+
gpu_vram_gb = torch.cuda.get_device_properties(0).total_memory / (1024 ** 3)
|
|
204
|
+
except ImportError:
|
|
205
|
+
pass
|
|
206
|
+
|
|
207
|
+
# Try Apple Silicon (Metal/MPS)
|
|
208
|
+
if not gpu_available and platform.system() == "Darwin":
|
|
209
|
+
try:
|
|
210
|
+
import subprocess
|
|
211
|
+
result = subprocess.run(
|
|
212
|
+
["system_profiler", "SPDisplaysDataType"],
|
|
213
|
+
capture_output=True, text=True, timeout=10
|
|
214
|
+
)
|
|
215
|
+
if result.returncode == 0:
|
|
216
|
+
output = result.stdout
|
|
217
|
+
if "Apple" in output:
|
|
218
|
+
gpu_available = True
|
|
219
|
+
# Extract GPU name
|
|
220
|
+
for line in output.split("\n"):
|
|
221
|
+
if "Chipset Model" in line:
|
|
222
|
+
gpu_name = line.split(":", 1)[1].strip()
|
|
223
|
+
break
|
|
224
|
+
# Apple Silicon shares unified memory
|
|
225
|
+
ram_info = get_ram_info()
|
|
226
|
+
gpu_vram_gb = ram_info.get("ram_total_gb", 0) * 0.75 # Estimate 75% available for GPU
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
"gpu_name": gpu_name,
|
|
232
|
+
"gpu_vram_gb": round(gpu_vram_gb, 2),
|
|
233
|
+
"gpu_available": gpu_available,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def get_disk_info(path: str = ".") -> Dict[str, float]:
|
|
238
|
+
"""Return disk space information for the given path."""
|
|
239
|
+
try:
|
|
240
|
+
import psutil
|
|
241
|
+
usage = psutil.disk_usage(path)
|
|
242
|
+
disk_total_gb = usage.total / (1024 ** 3)
|
|
243
|
+
disk_free_gb = usage.free / (1024 ** 3)
|
|
244
|
+
disk_used_gb = usage.used / (1024 ** 3)
|
|
245
|
+
disk_percent = usage.percent
|
|
246
|
+
except ImportError:
|
|
247
|
+
# Fallback using shutil
|
|
248
|
+
total, used, free = shutil.disk_usage(path)
|
|
249
|
+
disk_total_gb = total / (1024 ** 3)
|
|
250
|
+
disk_free_gb = free / (1024 ** 3)
|
|
251
|
+
disk_used_gb = used / (1024 ** 3)
|
|
252
|
+
disk_percent = (used / total * 100) if total > 0 else 0
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
"disk_total_gb": round(disk_total_gb, 2),
|
|
256
|
+
"disk_free_gb": round(disk_free_gb, 2),
|
|
257
|
+
"disk_used_gb": round(disk_used_gb, 2),
|
|
258
|
+
"disk_percent": round(disk_percent, 2),
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def detect_hardware() -> Dict[str, Any]:
|
|
263
|
+
"""Detect all hardware information and return a complete profile.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Dictionary containing:
|
|
267
|
+
- os: Operating system string
|
|
268
|
+
- cpu_count: Number of logical CPU cores
|
|
269
|
+
- cpu_model: CPU model name
|
|
270
|
+
- ram_total_gb: Total RAM in GB
|
|
271
|
+
- ram_available_gb: Available RAM in GB
|
|
272
|
+
- gpu_name: GPU name if available
|
|
273
|
+
- gpu_vram_gb: GPU VRAM in GB
|
|
274
|
+
- disk_free_gb: Free disk space in GB
|
|
275
|
+
"""
|
|
276
|
+
os_info = get_os_info()
|
|
277
|
+
cpu_info = get_cpu_info()
|
|
278
|
+
ram_info = get_ram_info()
|
|
279
|
+
gpu_info = get_gpu_info()
|
|
280
|
+
disk_info = get_disk_info()
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
"os": os_info,
|
|
284
|
+
"cpu_count": cpu_info["cpu_count"],
|
|
285
|
+
"cpu_model": cpu_info["cpu_model"],
|
|
286
|
+
"ram_total_gb": ram_info["ram_total_gb"],
|
|
287
|
+
"ram_available_gb": ram_info["ram_available_gb"],
|
|
288
|
+
"gpu_name": gpu_info["gpu_name"],
|
|
289
|
+
"gpu_vram_gb": gpu_info["gpu_vram_gb"],
|
|
290
|
+
"gpu_available": gpu_info["gpu_available"],
|
|
291
|
+
"disk_free_gb": disk_info["disk_free_gb"],
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def print_hardware_profile(profile: Optional[Dict[str, Any]] = None) -> None:
|
|
296
|
+
"""Print hardware profile in a formatted way."""
|
|
297
|
+
from .config import C_CYAN, C_RESET, C_DIM, C_GREEN, C_YELLOW, C_RED
|
|
298
|
+
|
|
299
|
+
if profile is None:
|
|
300
|
+
profile = detect_hardware()
|
|
301
|
+
|
|
302
|
+
print(f"\n{C_CYAN}{'='*60}{C_RESET}")
|
|
303
|
+
print(f"{C_CYAN}HARDWARE PROFILE{C_RESET}")
|
|
304
|
+
print(f"{C_CYAN}{'='*60}{C_RESET}\n")
|
|
305
|
+
|
|
306
|
+
print(f"{C_GREEN}Operating System:{C_RESET}")
|
|
307
|
+
print(f" {profile['os']}\n")
|
|
308
|
+
|
|
309
|
+
print(f"{C_GREEN}CPU:{C_RESET}")
|
|
310
|
+
print(f" Model: {profile['cpu_model']}")
|
|
311
|
+
print(f" Cores: {profile['cpu_count']} logical\n")
|
|
312
|
+
|
|
313
|
+
print(f"{C_GREEN}Memory (RAM):{C_RESET}")
|
|
314
|
+
if profile['ram_total_gb'] > 0:
|
|
315
|
+
print(f" Total: {profile['ram_total_gb']:.2f} GB")
|
|
316
|
+
print(f" Available: {profile['ram_available_gb']:.2f} GB")
|
|
317
|
+
print(f" Usage: {profile.get('ram_percent', 0):.1f}%\n")
|
|
318
|
+
else:
|
|
319
|
+
print(f" {C_DIM}Unable to detect RAM{C_RESET}\n")
|
|
320
|
+
|
|
321
|
+
print(f"{C_GREEN}GPU:{C_RESET}")
|
|
322
|
+
if profile['gpu_available']:
|
|
323
|
+
print(f" Name: {profile['gpu_name']}")
|
|
324
|
+
print(f" VRAM: {profile['gpu_vram_gb']:.2f} GB{C_GREEN} [GPU acceleration possible]{C_RESET}\n")
|
|
325
|
+
else:
|
|
326
|
+
print(f" {C_DIM}No dedicated GPU detected (CPU-only mode){C_RESET}\n")
|
|
327
|
+
|
|
328
|
+
print(f"{C_GREEN}Storage:{C_RESET}")
|
|
329
|
+
print(f" Free Space: {profile['disk_free_gb']:.2f} GB\n")
|
|
330
|
+
|
|
331
|
+
# Capacity assessment
|
|
332
|
+
print(f"{C_CYAN}{'='*60}{C_RESET}")
|
|
333
|
+
print(f"{C_CYAN}EXECUTION CAPACITY ASSESSMENT{C_RESET}")
|
|
334
|
+
print(f"{C_CYAN}{'='*60}{C_RESET}\n")
|
|
335
|
+
|
|
336
|
+
ram = profile['ram_available_gb']
|
|
337
|
+
vram = profile['gpu_vram_gb']
|
|
338
|
+
|
|
339
|
+
if profile['gpu_available'] and vram >= 6:
|
|
340
|
+
print(f"{C_GREEN}✓ GPU Acceleration:{C_RESET} Available with {vram:.1f} GB VRAM")
|
|
341
|
+
print(f" → Can run larger models with GPU offloading\n")
|
|
342
|
+
elif profile['gpu_available']:
|
|
343
|
+
print(f"{C_YELLOW}⚠ GPU Limited:{C_RESET} Only {vram:.1f} GB VRAM available")
|
|
344
|
+
print(f" → Small models or partial GPU offloading recommended\n")
|
|
345
|
+
else:
|
|
346
|
+
print(f"{C_DIM}○ CPU-Only Mode:{C_RESET} No GPU detected")
|
|
347
|
+
print(f" → All computation will use CPU\n")
|
|
348
|
+
|
|
349
|
+
if ram < 4:
|
|
350
|
+
print(f"{C_RED}✗ Memory Critical:{C_RESET} Only {ram:.1f} GB available")
|
|
351
|
+
print(f" → Very limited model options (≤1.5B parameters)\n")
|
|
352
|
+
elif ram < 8:
|
|
353
|
+
print(f"{C_YELLOW}⚠ Memory Limited:{C_RESET} {ram:.1f} GB available")
|
|
354
|
+
print(f" → Models up to 7B (quantized) should work\n")
|
|
355
|
+
else:
|
|
356
|
+
print(f"{C_GREEN}✓ Memory Adequate:{C_RESET} {ram:.1f} GB available")
|
|
357
|
+
print(f" → Can run models up to 14B+ (quantized)\n")
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
if __name__ == "__main__":
|
|
361
|
+
print_hardware_profile()
|