mita-code 0.1.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.
- mita/__init__.py +5 -0
- mita/__main__.py +5 -0
- mita/agent/__init__.py +1 -0
- mita/agent/context.py +43 -0
- mita/agent/conversation.py +101 -0
- mita/agent/loop.py +594 -0
- mita/agent/system_prompt.py +75 -0
- mita/cli.py +940 -0
- mita/config/__init__.py +6 -0
- mita/config/defaults.py +41 -0
- mita/config/loader.py +53 -0
- mita/config/schema.py +131 -0
- mita/hooks/__init__.py +1 -0
- mita/hooks/manager.py +94 -0
- mita/hooks/runner.py +145 -0
- mita/index/__init__.py +1 -0
- mita/index/embeddings.py +49 -0
- mita/index/manager.py +170 -0
- mita/index/parser.py +331 -0
- mita/index/retriever.py +53 -0
- mita/index/store.py +143 -0
- mita/llm/__init__.py +1 -0
- mita/llm/client.py +86 -0
- mita/llm/instructor.py +80 -0
- mita/llm/streaming.py +58 -0
- mita/memory/__init__.py +6 -0
- mita/memory/discovery.py +61 -0
- mita/memory/loader.py +76 -0
- mita/memory/manager.py +117 -0
- mita/models/__init__.py +13 -0
- mita/models/hardware.py +289 -0
- mita/models/manager.py +268 -0
- mita/models/ollama_client.py +104 -0
- mita/models/recommender.py +88 -0
- mita/models/registry.py +167 -0
- mita/models/server.py +262 -0
- mita/plugins/__init__.py +1 -0
- mita/plugins/client.py +152 -0
- mita/plugins/manager.py +210 -0
- mita/py.typed +0 -0
- mita/skills/__init__.py +1 -0
- mita/skills/executor.py +84 -0
- mita/skills/loader.py +117 -0
- mita/skills/manager.py +129 -0
- mita/tools/__init__.py +1 -0
- mita/tools/builtins/__init__.py +28 -0
- mita/tools/builtins/file_edit.py +71 -0
- mita/tools/builtins/file_read.py +74 -0
- mita/tools/builtins/file_write.py +42 -0
- mita/tools/builtins/git.py +112 -0
- mita/tools/builtins/glob_tool.py +67 -0
- mita/tools/builtins/grep_tool.py +93 -0
- mita/tools/builtins/shell.py +83 -0
- mita/tools/executor.py +80 -0
- mita/tools/registry.py +69 -0
- mita/tools/safety.py +91 -0
- mita/tools/schema.py +87 -0
- mita/ui/__init__.py +1 -0
- mita/ui/display.py +139 -0
- mita/ui/repl.py +88 -0
- mita/ui/spinner.py +48 -0
- mita/ui/theme.py +23 -0
- mita_code-0.1.0.dist-info/METADATA +227 -0
- mita_code-0.1.0.dist-info/RECORD +67 -0
- mita_code-0.1.0.dist-info/WHEEL +4 -0
- mita_code-0.1.0.dist-info/entry_points.txt +3 -0
- mita_code-0.1.0.dist-info/licenses/LICENSE +201 -0
mita/models/hardware.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Hardware detection: RAM, VRAM, CPU, GPU."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import platform
|
|
6
|
+
import subprocess
|
|
7
|
+
|
|
8
|
+
import psutil
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GPUInfo(BaseModel):
|
|
13
|
+
"""Information about a detected GPU."""
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
vram_gb: float
|
|
17
|
+
vendor: str # "nvidia", "amd", "apple"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HardwareInfo(BaseModel):
|
|
21
|
+
"""Detected hardware capabilities."""
|
|
22
|
+
|
|
23
|
+
ram_gb: float
|
|
24
|
+
cpu_cores: int
|
|
25
|
+
cpu_name: str
|
|
26
|
+
gpus: list[GPUInfo] = Field(default_factory=list)
|
|
27
|
+
os: str # "darwin", "linux", "windows"
|
|
28
|
+
apple_silicon: bool = False
|
|
29
|
+
unified_memory: bool = False
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def available_vram_gb(self) -> float:
|
|
33
|
+
"""Effective VRAM available for models.
|
|
34
|
+
|
|
35
|
+
On Apple Silicon with unified memory, most of RAM is usable as VRAM.
|
|
36
|
+
On discrete GPUs, use the GPU VRAM.
|
|
37
|
+
Falls back to a conservative fraction of RAM for CPU-only inference.
|
|
38
|
+
"""
|
|
39
|
+
if self.unified_memory:
|
|
40
|
+
# Apple Silicon can use ~75% of unified memory for ML
|
|
41
|
+
return self.ram_gb * 0.75
|
|
42
|
+
if self.gpus:
|
|
43
|
+
# Use the largest single GPU — Ollama loads a model into one GPU
|
|
44
|
+
return max(gpu.vram_gb for gpu in self.gpus)
|
|
45
|
+
# CPU-only: models load into RAM, leave room for OS
|
|
46
|
+
return max(0, self.ram_gb - 4)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def detect_hardware() -> HardwareInfo:
|
|
50
|
+
"""Detect hardware capabilities of the current machine."""
|
|
51
|
+
system = platform.system().lower()
|
|
52
|
+
os_name = {"darwin": "darwin", "linux": "linux", "windows": "windows"}.get(system, system)
|
|
53
|
+
|
|
54
|
+
ram_gb = round(psutil.virtual_memory().total / (1024**3), 1)
|
|
55
|
+
cpu_cores = psutil.cpu_count(logical=False) or psutil.cpu_count() or 1
|
|
56
|
+
cpu_name = _detect_cpu_name()
|
|
57
|
+
|
|
58
|
+
apple_silicon = False
|
|
59
|
+
unified_memory = False
|
|
60
|
+
gpus: list[GPUInfo] = []
|
|
61
|
+
|
|
62
|
+
if os_name == "darwin":
|
|
63
|
+
apple_silicon, unified_memory, gpus = _detect_macos_gpu(ram_gb)
|
|
64
|
+
elif os_name == "linux":
|
|
65
|
+
gpus = _detect_linux_gpu()
|
|
66
|
+
|
|
67
|
+
return HardwareInfo(
|
|
68
|
+
ram_gb=ram_gb,
|
|
69
|
+
cpu_cores=cpu_cores,
|
|
70
|
+
cpu_name=cpu_name,
|
|
71
|
+
gpus=gpus,
|
|
72
|
+
os=os_name,
|
|
73
|
+
apple_silicon=apple_silicon,
|
|
74
|
+
unified_memory=unified_memory,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _detect_cpu_name() -> str:
|
|
79
|
+
"""Detect the CPU model name."""
|
|
80
|
+
system = platform.system().lower()
|
|
81
|
+
|
|
82
|
+
if system == "darwin":
|
|
83
|
+
try:
|
|
84
|
+
result = subprocess.run(
|
|
85
|
+
["sysctl", "-n", "machdep.cpu.brand_string"],
|
|
86
|
+
capture_output=True,
|
|
87
|
+
text=True,
|
|
88
|
+
timeout=5,
|
|
89
|
+
)
|
|
90
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
91
|
+
return result.stdout.strip()
|
|
92
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
93
|
+
pass
|
|
94
|
+
# Apple Silicon doesn't have brand_string, use chip name
|
|
95
|
+
try:
|
|
96
|
+
result = subprocess.run(
|
|
97
|
+
["sysctl", "-n", "hw.chip"],
|
|
98
|
+
capture_output=True,
|
|
99
|
+
text=True,
|
|
100
|
+
timeout=5,
|
|
101
|
+
)
|
|
102
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
103
|
+
return result.stdout.strip()
|
|
104
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
105
|
+
pass
|
|
106
|
+
|
|
107
|
+
elif system == "linux":
|
|
108
|
+
try:
|
|
109
|
+
with open("/proc/cpuinfo") as f:
|
|
110
|
+
for line in f:
|
|
111
|
+
if line.startswith("model name"):
|
|
112
|
+
return line.split(":", 1)[1].strip()
|
|
113
|
+
except (OSError, IndexError):
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
return platform.processor() or "Unknown CPU"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _is_apple_silicon() -> bool:
|
|
120
|
+
"""Detect Apple Silicon, even under Rosetta emulation."""
|
|
121
|
+
machine = platform.machine().lower()
|
|
122
|
+
if machine in ("arm64", "aarch64"):
|
|
123
|
+
return True
|
|
124
|
+
# Under Rosetta, platform.machine() returns x86_64.
|
|
125
|
+
# Check sysctl for the actual hardware.
|
|
126
|
+
try:
|
|
127
|
+
result = subprocess.run(
|
|
128
|
+
["sysctl", "-n", "hw.optional.arm64"],
|
|
129
|
+
capture_output=True,
|
|
130
|
+
text=True,
|
|
131
|
+
timeout=5,
|
|
132
|
+
)
|
|
133
|
+
if result.returncode == 0 and result.stdout.strip() == "1":
|
|
134
|
+
return True
|
|
135
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
136
|
+
pass
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _detect_macos_gpu(ram_gb: float) -> tuple[bool, bool, list[GPUInfo]]:
|
|
141
|
+
"""Detect GPU on macOS. Returns (apple_silicon, unified_memory, gpus)."""
|
|
142
|
+
apple_silicon = _is_apple_silicon()
|
|
143
|
+
|
|
144
|
+
if apple_silicon:
|
|
145
|
+
# Apple Silicon has unified memory — RAM is shared with GPU
|
|
146
|
+
chip_name = _get_apple_chip_name()
|
|
147
|
+
return (
|
|
148
|
+
True,
|
|
149
|
+
True,
|
|
150
|
+
[
|
|
151
|
+
GPUInfo(
|
|
152
|
+
name=chip_name,
|
|
153
|
+
vram_gb=ram_gb, # Unified — all RAM is addressable
|
|
154
|
+
vendor="apple",
|
|
155
|
+
)
|
|
156
|
+
],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Intel Mac — try to detect discrete GPU via system_profiler
|
|
160
|
+
gpus = _detect_macos_discrete_gpu()
|
|
161
|
+
return False, False, gpus
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _get_apple_chip_name() -> str:
|
|
165
|
+
"""Get the Apple Silicon chip name (e.g., 'Apple M2 Pro')."""
|
|
166
|
+
try:
|
|
167
|
+
result = subprocess.run(
|
|
168
|
+
["sysctl", "-n", "machdep.cpu.brand_string"],
|
|
169
|
+
capture_output=True,
|
|
170
|
+
text=True,
|
|
171
|
+
timeout=5,
|
|
172
|
+
)
|
|
173
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
174
|
+
return result.stdout.strip()
|
|
175
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
176
|
+
pass
|
|
177
|
+
return "Apple Silicon"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _detect_macos_discrete_gpu() -> list[GPUInfo]:
|
|
181
|
+
"""Detect discrete GPUs on Intel Macs via system_profiler."""
|
|
182
|
+
try:
|
|
183
|
+
result = subprocess.run(
|
|
184
|
+
["system_profiler", "SPDisplaysDataType", "-detailLevel", "basic"],
|
|
185
|
+
capture_output=True,
|
|
186
|
+
text=True,
|
|
187
|
+
timeout=10,
|
|
188
|
+
)
|
|
189
|
+
if result.returncode != 0:
|
|
190
|
+
return []
|
|
191
|
+
|
|
192
|
+
gpus: list[GPUInfo] = []
|
|
193
|
+
current_name = ""
|
|
194
|
+
for line in result.stdout.splitlines():
|
|
195
|
+
stripped = line.strip()
|
|
196
|
+
if stripped.startswith("Chipset Model:"):
|
|
197
|
+
current_name = stripped.split(":", 1)[1].strip()
|
|
198
|
+
elif stripped.startswith("VRAM") and current_name:
|
|
199
|
+
vram_str = stripped.split(":", 1)[1].strip()
|
|
200
|
+
vram_gb = _parse_vram_string(vram_str)
|
|
201
|
+
vendor = _guess_gpu_vendor(current_name)
|
|
202
|
+
gpus.append(GPUInfo(name=current_name, vram_gb=vram_gb, vendor=vendor))
|
|
203
|
+
current_name = ""
|
|
204
|
+
return gpus
|
|
205
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
206
|
+
return []
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _detect_linux_gpu() -> list[GPUInfo]:
|
|
210
|
+
"""Detect NVIDIA GPUs on Linux via nvidia-smi."""
|
|
211
|
+
gpus: list[GPUInfo] = []
|
|
212
|
+
|
|
213
|
+
# Try nvidia-smi
|
|
214
|
+
try:
|
|
215
|
+
result = subprocess.run(
|
|
216
|
+
[
|
|
217
|
+
"nvidia-smi",
|
|
218
|
+
"--query-gpu=name,memory.total",
|
|
219
|
+
"--format=csv,noheader,nounits",
|
|
220
|
+
],
|
|
221
|
+
capture_output=True,
|
|
222
|
+
text=True,
|
|
223
|
+
timeout=10,
|
|
224
|
+
)
|
|
225
|
+
if result.returncode == 0:
|
|
226
|
+
for line in result.stdout.strip().splitlines():
|
|
227
|
+
parts = line.split(",")
|
|
228
|
+
if len(parts) >= 2:
|
|
229
|
+
name = parts[0].strip()
|
|
230
|
+
vram_mb = float(parts[1].strip())
|
|
231
|
+
gpus.append(
|
|
232
|
+
GPUInfo(name=name, vram_gb=round(vram_mb / 1024, 1), vendor="nvidia")
|
|
233
|
+
)
|
|
234
|
+
except (subprocess.SubprocessError, FileNotFoundError, ValueError):
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
# Try AMD ROCm
|
|
238
|
+
if not gpus:
|
|
239
|
+
try:
|
|
240
|
+
result = subprocess.run(
|
|
241
|
+
["rocm-smi", "--showmeminfo", "vram", "--csv"],
|
|
242
|
+
capture_output=True,
|
|
243
|
+
text=True,
|
|
244
|
+
timeout=10,
|
|
245
|
+
)
|
|
246
|
+
if result.returncode == 0:
|
|
247
|
+
for line in result.stdout.strip().splitlines()[1:]: # skip header
|
|
248
|
+
parts = line.split(",")
|
|
249
|
+
if len(parts) >= 2:
|
|
250
|
+
try:
|
|
251
|
+
vram_bytes = int(parts[1].strip())
|
|
252
|
+
gpus.append(
|
|
253
|
+
GPUInfo(
|
|
254
|
+
name="AMD GPU",
|
|
255
|
+
vram_gb=round(vram_bytes / (1024**3), 1),
|
|
256
|
+
vendor="amd",
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
except ValueError:
|
|
260
|
+
pass
|
|
261
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
262
|
+
pass
|
|
263
|
+
|
|
264
|
+
return gpus
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _parse_vram_string(vram_str: str) -> float:
|
|
268
|
+
"""Parse VRAM strings like '8 GB', '4096 MB' into GB."""
|
|
269
|
+
vram_str = vram_str.strip().upper()
|
|
270
|
+
try:
|
|
271
|
+
if "GB" in vram_str:
|
|
272
|
+
return float(vram_str.replace("GB", "").strip())
|
|
273
|
+
if "MB" in vram_str:
|
|
274
|
+
return round(float(vram_str.replace("MB", "").strip()) / 1024, 1)
|
|
275
|
+
except ValueError:
|
|
276
|
+
pass
|
|
277
|
+
return 0.0
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _guess_gpu_vendor(name: str) -> str:
|
|
281
|
+
"""Guess GPU vendor from the chipset name."""
|
|
282
|
+
name_lower = name.lower()
|
|
283
|
+
if "nvidia" in name_lower or "geforce" in name_lower or "quadro" in name_lower:
|
|
284
|
+
return "nvidia"
|
|
285
|
+
if "amd" in name_lower or "radeon" in name_lower:
|
|
286
|
+
return "amd"
|
|
287
|
+
if "intel" in name_lower:
|
|
288
|
+
return "intel"
|
|
289
|
+
return "unknown"
|
mita/models/manager.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""CLI command handlers for model management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.progress import BarColumn, DownloadColumn, Progress, TextColumn, TransferSpeedColumn
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from mita.config.loader import load_config
|
|
11
|
+
from mita.models.hardware import detect_hardware
|
|
12
|
+
from mita.models.ollama_client import OllamaClient
|
|
13
|
+
from mita.models.recommender import recommend_models
|
|
14
|
+
from mita.models.registry import find_model
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_client() -> OllamaClient:
|
|
20
|
+
"""Get an OllamaClient from the current config."""
|
|
21
|
+
cfg = load_config()
|
|
22
|
+
return OllamaClient(host=cfg.ollama.host, timeout=cfg.ollama.timeout)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _check_ollama(client: OllamaClient) -> bool:
|
|
26
|
+
"""Check if Ollama is running, auto-start if configured."""
|
|
27
|
+
if client.is_running():
|
|
28
|
+
return True
|
|
29
|
+
|
|
30
|
+
# Try auto-start if configured
|
|
31
|
+
cfg = load_config()
|
|
32
|
+
if cfg.ollama.auto_manage:
|
|
33
|
+
from mita.models.server import ensure_server
|
|
34
|
+
|
|
35
|
+
if ensure_server(host=cfg.ollama.host, auto_manage=True, console=console):
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
console.print(
|
|
39
|
+
"[red]Cannot connect to Ollama.[/red]\n"
|
|
40
|
+
"Make sure Ollama is running: [bold]ollama serve[/bold]"
|
|
41
|
+
)
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def list_models() -> None:
|
|
46
|
+
"""List all installed Ollama models."""
|
|
47
|
+
client = _get_client()
|
|
48
|
+
if not _check_ollama(client):
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
models = client.list_models()
|
|
52
|
+
if not models:
|
|
53
|
+
console.print(
|
|
54
|
+
"[dim]No models installed. "
|
|
55
|
+
"Run [bold]mita models pull <name>[/bold] to get started.[/dim]"
|
|
56
|
+
)
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
table = Table(title="Installed Models")
|
|
60
|
+
table.add_column("Name", style="bold")
|
|
61
|
+
table.add_column("Size", justify="right")
|
|
62
|
+
table.add_column("Parameters", justify="right")
|
|
63
|
+
table.add_column("Quantization")
|
|
64
|
+
table.add_column("Family")
|
|
65
|
+
|
|
66
|
+
for m in models:
|
|
67
|
+
table.add_row(
|
|
68
|
+
m.name,
|
|
69
|
+
f"{m.size_gb:.1f} GB",
|
|
70
|
+
m.parameter_size,
|
|
71
|
+
m.quantization,
|
|
72
|
+
m.family,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
console.print(table)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def pull_model(name: str) -> None:
|
|
79
|
+
"""Pull a model from the Ollama registry."""
|
|
80
|
+
import ollama as ollama_lib
|
|
81
|
+
|
|
82
|
+
client = _get_client()
|
|
83
|
+
if not _check_ollama(client):
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
console.print(f"Pulling [bold]{name}[/bold]...")
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
with Progress(
|
|
90
|
+
TextColumn("[progress.description]{task.description}"),
|
|
91
|
+
BarColumn(),
|
|
92
|
+
DownloadColumn(),
|
|
93
|
+
TransferSpeedColumn(),
|
|
94
|
+
console=console,
|
|
95
|
+
) as progress:
|
|
96
|
+
task = progress.add_task("Downloading", total=None)
|
|
97
|
+
|
|
98
|
+
for update in client.pull(name, stream=True):
|
|
99
|
+
status = update.get("status", "")
|
|
100
|
+
completed = update.get("completed", 0)
|
|
101
|
+
total = update.get("total", 0)
|
|
102
|
+
|
|
103
|
+
if total > 0:
|
|
104
|
+
progress.update(task, completed=completed, total=total, description=status)
|
|
105
|
+
else:
|
|
106
|
+
progress.update(task, description=status)
|
|
107
|
+
|
|
108
|
+
progress.update(task, description="Complete")
|
|
109
|
+
|
|
110
|
+
console.print(f"[green]Successfully pulled {name}[/green]")
|
|
111
|
+
except ollama_lib.ResponseError as e:
|
|
112
|
+
console.print(f"[red]Failed to pull {name}: {e}[/red]")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def remove_model(name: str) -> None:
|
|
116
|
+
"""Remove an installed model."""
|
|
117
|
+
client = _get_client()
|
|
118
|
+
if not _check_ollama(client):
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
import ollama as ollama_lib
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
client.remove(name)
|
|
125
|
+
console.print(f"[green]Removed {name}[/green]")
|
|
126
|
+
except ollama_lib.ResponseError as e:
|
|
127
|
+
console.print(f"[red]Failed to remove {name}: {e}[/red]")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def show_model_info(name: str) -> None:
|
|
131
|
+
"""Show details about a model."""
|
|
132
|
+
# Check registry first
|
|
133
|
+
card = find_model(name)
|
|
134
|
+
if card:
|
|
135
|
+
table = Table(title=f"Registry: {name}", show_header=False)
|
|
136
|
+
table.add_column("Field", style="bold")
|
|
137
|
+
table.add_column("Value")
|
|
138
|
+
table.add_row("Family", card.family)
|
|
139
|
+
table.add_row("Parameters", card.param_count)
|
|
140
|
+
table.add_row("Context Window", f"{card.context_window:,}")
|
|
141
|
+
table.add_row("Min RAM", f"{card.min_ram_gb:.0f} GB")
|
|
142
|
+
table.add_row("Min VRAM", f"{card.min_vram_gb:.0f} GB")
|
|
143
|
+
table.add_row("Tool Calls", "Yes" if card.tool_call_support else "No")
|
|
144
|
+
table.add_row("Quantization", card.quantization)
|
|
145
|
+
table.add_row("Description", card.description)
|
|
146
|
+
table.add_row("Tags", ", ".join(card.tags))
|
|
147
|
+
console.print(table)
|
|
148
|
+
console.print()
|
|
149
|
+
|
|
150
|
+
# Also show Ollama details if installed
|
|
151
|
+
import ollama as ollama_lib
|
|
152
|
+
|
|
153
|
+
client = _get_client()
|
|
154
|
+
if not _check_ollama(client):
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
info = client.show(name)
|
|
159
|
+
details = info.get("details", {})
|
|
160
|
+
if isinstance(details, dict) and details:
|
|
161
|
+
table = Table(title=f"Ollama: {name}", show_header=False)
|
|
162
|
+
table.add_column("Field", style="bold")
|
|
163
|
+
table.add_column("Value")
|
|
164
|
+
for k, v in details.items():
|
|
165
|
+
table.add_row(str(k), str(v))
|
|
166
|
+
console.print(table)
|
|
167
|
+
elif not card:
|
|
168
|
+
console.print(f"[dim]Model {name} is installed but not in the curated registry.[/dim]")
|
|
169
|
+
except ollama_lib.ResponseError as e:
|
|
170
|
+
if not card:
|
|
171
|
+
console.print(f"[red]Model {name} not found in registry or Ollama: {e}[/red]")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def show_recommendations() -> None:
|
|
175
|
+
"""Detect hardware and recommend models."""
|
|
176
|
+
hw = detect_hardware()
|
|
177
|
+
|
|
178
|
+
# Show hardware summary
|
|
179
|
+
hw_panel = Panel(
|
|
180
|
+
f"[bold]CPU:[/bold] {hw.cpu_name} ({hw.cpu_cores} cores)\n"
|
|
181
|
+
f"[bold]RAM:[/bold] {hw.ram_gb:.1f} GB\n"
|
|
182
|
+
f"[bold]GPU:[/bold] {hw.gpus[0].name if hw.gpus else 'None detected'}\n"
|
|
183
|
+
f"[bold]VRAM:[/bold] {hw.available_vram_gb:.1f} GB effective\n"
|
|
184
|
+
f"[bold]OS:[/bold] {hw.os}"
|
|
185
|
+
+ ("\n[bold]Unified Memory:[/bold] Yes" if hw.unified_memory else ""),
|
|
186
|
+
title="Hardware Detected",
|
|
187
|
+
border_style="blue",
|
|
188
|
+
)
|
|
189
|
+
console.print(hw_panel)
|
|
190
|
+
console.print()
|
|
191
|
+
|
|
192
|
+
# Show recommendations
|
|
193
|
+
recs = recommend_models(hw)
|
|
194
|
+
if not recs:
|
|
195
|
+
console.print(
|
|
196
|
+
"[yellow]No models in the registry fit your hardware.[/yellow]\n"
|
|
197
|
+
"You may need more RAM/VRAM, or try a smaller model manually."
|
|
198
|
+
)
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
table = Table(title="Recommended Models")
|
|
202
|
+
table.add_column("Model", style="bold")
|
|
203
|
+
table.add_column("Params", justify="right")
|
|
204
|
+
table.add_column("Context", justify="right")
|
|
205
|
+
table.add_column("Fit", justify="right")
|
|
206
|
+
table.add_column("Notes")
|
|
207
|
+
|
|
208
|
+
for rec in recs:
|
|
209
|
+
# Color-code fit score
|
|
210
|
+
score = rec.fit_score
|
|
211
|
+
if score >= 0.8:
|
|
212
|
+
fit_style = "green"
|
|
213
|
+
elif score >= 0.6:
|
|
214
|
+
fit_style = "yellow"
|
|
215
|
+
else:
|
|
216
|
+
fit_style = "red"
|
|
217
|
+
|
|
218
|
+
table.add_row(
|
|
219
|
+
rec.model.name,
|
|
220
|
+
rec.model.param_count,
|
|
221
|
+
f"{rec.model.context_window:,}",
|
|
222
|
+
f"[{fit_style}]{score:.0%}[/{fit_style}]",
|
|
223
|
+
rec.notes,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
console.print(table)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def set_default_model(name: str) -> None:
|
|
230
|
+
"""Set the default model (prints instruction since config is TOML-based)."""
|
|
231
|
+
card = find_model(name)
|
|
232
|
+
if card:
|
|
233
|
+
console.print(f"[green]Model [bold]{name}[/bold] is in the registry.[/green]")
|
|
234
|
+
else:
|
|
235
|
+
console.print(f"[yellow]Model [bold]{name}[/bold] is not in the curated registry.[/yellow]")
|
|
236
|
+
|
|
237
|
+
console.print(
|
|
238
|
+
f"\nTo set as default, add to your config:\n\n"
|
|
239
|
+
f' [bold]mita config set model.default "{name}"[/bold]\n\n'
|
|
240
|
+
f"Or edit your config file directly:\n\n"
|
|
241
|
+
f' [dim][model]\n default = "{name}"[/dim]'
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def show_hardware() -> None:
|
|
246
|
+
"""Show detected hardware information."""
|
|
247
|
+
hw = detect_hardware()
|
|
248
|
+
|
|
249
|
+
table = Table(title="Hardware Information", show_header=False)
|
|
250
|
+
table.add_column("Field", style="bold")
|
|
251
|
+
table.add_column("Value")
|
|
252
|
+
table.add_row("OS", hw.os)
|
|
253
|
+
table.add_row("CPU", hw.cpu_name)
|
|
254
|
+
table.add_row("CPU Cores", str(hw.cpu_cores))
|
|
255
|
+
table.add_row("RAM", f"{hw.ram_gb:.1f} GB")
|
|
256
|
+
table.add_row("Apple Silicon", "Yes" if hw.apple_silicon else "No")
|
|
257
|
+
table.add_row("Unified Memory", "Yes" if hw.unified_memory else "No")
|
|
258
|
+
|
|
259
|
+
for i, gpu in enumerate(hw.gpus):
|
|
260
|
+
prefix = f"GPU {i}" if len(hw.gpus) > 1 else "GPU"
|
|
261
|
+
table.add_row(prefix, f"{gpu.name} ({gpu.vram_gb:.1f} GB, {gpu.vendor})")
|
|
262
|
+
|
|
263
|
+
if not hw.gpus:
|
|
264
|
+
table.add_row("GPU", "None detected")
|
|
265
|
+
|
|
266
|
+
table.add_row("Effective VRAM", f"{hw.available_vram_gb:.1f} GB")
|
|
267
|
+
|
|
268
|
+
console.print(table)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Wrapper around the Ollama Python client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import ollama
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OllamaModelInfo(BaseModel):
|
|
13
|
+
"""Info about an installed Ollama model."""
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
size_gb: float
|
|
17
|
+
parameter_size: str = ""
|
|
18
|
+
quantization: str = ""
|
|
19
|
+
family: str = ""
|
|
20
|
+
modified_at: str = ""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OllamaClient:
|
|
24
|
+
"""Thin wrapper around the ollama Python client."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, host: str = "http://localhost:11434", timeout: int = 120) -> None:
|
|
27
|
+
self._client = ollama.Client(host=host, timeout=timeout)
|
|
28
|
+
self._host = host
|
|
29
|
+
|
|
30
|
+
def is_running(self) -> bool:
|
|
31
|
+
"""Check if the Ollama server is reachable."""
|
|
32
|
+
try:
|
|
33
|
+
self._client.list()
|
|
34
|
+
return True
|
|
35
|
+
except (ConnectionError, OSError, ollama.ResponseError, TimeoutError):
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
def list_models(self) -> list[OllamaModelInfo]:
|
|
39
|
+
"""List all installed models."""
|
|
40
|
+
response = self._client.list()
|
|
41
|
+
models: list[OllamaModelInfo] = []
|
|
42
|
+
for m in response.models:
|
|
43
|
+
details: Any = m.details or {}
|
|
44
|
+
size_gb = round((m.size or 0) / (1024**3), 2)
|
|
45
|
+
|
|
46
|
+
# Handle details as either dict or object
|
|
47
|
+
if isinstance(details, dict):
|
|
48
|
+
param_size = details.get("parameter_size", "")
|
|
49
|
+
quant = details.get("quantization_level", "")
|
|
50
|
+
family = details.get("family", "")
|
|
51
|
+
else:
|
|
52
|
+
param_size = getattr(details, "parameter_size", "") or ""
|
|
53
|
+
quant = getattr(details, "quantization_level", "") or ""
|
|
54
|
+
family = getattr(details, "family", "") or ""
|
|
55
|
+
|
|
56
|
+
models.append(
|
|
57
|
+
OllamaModelInfo(
|
|
58
|
+
name=m.model or "",
|
|
59
|
+
size_gb=size_gb,
|
|
60
|
+
parameter_size=param_size,
|
|
61
|
+
quantization=quant,
|
|
62
|
+
family=family,
|
|
63
|
+
modified_at=str(m.modified_at or ""),
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
return models
|
|
67
|
+
|
|
68
|
+
def pull(self, model_name: str, stream: bool = True) -> Iterator[dict[str, Any]]:
|
|
69
|
+
"""Pull a model from the Ollama registry.
|
|
70
|
+
|
|
71
|
+
Yields progress dicts with keys like 'status', 'completed', 'total'.
|
|
72
|
+
Raises ollama.ResponseError on failure (e.g., model not found).
|
|
73
|
+
"""
|
|
74
|
+
if stream:
|
|
75
|
+
response = self._client.pull(model_name, stream=True)
|
|
76
|
+
for chunk in response:
|
|
77
|
+
if isinstance(chunk, dict):
|
|
78
|
+
yield chunk
|
|
79
|
+
else:
|
|
80
|
+
yield {"status": getattr(chunk, "status", str(chunk))}
|
|
81
|
+
else:
|
|
82
|
+
self._client.pull(model_name, stream=False)
|
|
83
|
+
yield {"status": "success"}
|
|
84
|
+
|
|
85
|
+
def remove(self, model_name: str) -> None:
|
|
86
|
+
"""Remove an installed model.
|
|
87
|
+
|
|
88
|
+
Raises ollama.ResponseError if the model is not found.
|
|
89
|
+
"""
|
|
90
|
+
self._client.delete(model_name)
|
|
91
|
+
|
|
92
|
+
def show(self, model_name: str) -> dict[str, Any]:
|
|
93
|
+
"""Show details about a model."""
|
|
94
|
+
response = self._client.show(model_name)
|
|
95
|
+
if isinstance(response, dict):
|
|
96
|
+
return response
|
|
97
|
+
# Convert response object to dict
|
|
98
|
+
return {
|
|
99
|
+
"modelfile": getattr(response, "modelfile", ""),
|
|
100
|
+
"parameters": getattr(response, "parameters", ""),
|
|
101
|
+
"template": getattr(response, "template", ""),
|
|
102
|
+
"details": getattr(response, "details", {}),
|
|
103
|
+
"model_info": getattr(response, "model_info", {}),
|
|
104
|
+
}
|