k-cli-for-devs 1.0.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.
- k_cli/__init__.py +77 -0
- k_cli/agents/__init__.py +0 -0
- k_cli/agents/adversarial_swarm.py +338 -0
- k_cli/agents/agent_core.py +255 -0
- k_cli/agents/background_daemon.py +141 -0
- k_cli/agents/orchestrator.py +376 -0
- k_cli/agents/persona.py +649 -0
- k_cli/agents/scaffold_engine.py +121 -0
- k_cli/agents/strands_agent.py +832 -0
- k_cli/agents/subagents.py +1496 -0
- k_cli/cli.py +3297 -0
- k_cli/core/__init__.py +0 -0
- k_cli/core/airgap.py +95 -0
- k_cli/core/credentials.py +548 -0
- k_cli/core/intent_sensor.py +177 -0
- k_cli/core/llm_driver.py +1028 -0
- k_cli/core/model_manager.py +1109 -0
- k_cli/core/models_hub.py +913 -0
- k_cli/core/prompting.py +41 -0
- k_cli/core/sdk.py +322 -0
- k_cli/core/session.py +826 -0
- k_cli/core/smart_router.py +230 -0
- k_cli/core/storage_manager.py +176 -0
- k_cli/core/viewport_engine.py +117 -0
- k_cli/demo/demo_runner.py +579 -0
- k_cli/git/__init__.py +0 -0
- k_cli/git/ai_bisect.py +208 -0
- k_cli/git/conflict_resolver.py +1039 -0
- k_cli/git/git_guard.py +417 -0
- k_cli/git/patcher.py +1175 -0
- k_cli/git/repo_map.py +1780 -0
- k_cli/git/smart_git.py +928 -0
- k_cli/git/verifier.py +969 -0
- k_cli/github/__init__.py +0 -0
- k_cli/github/dedup_engine.py +787 -0
- k_cli/github/github_client.py +1702 -0
- k_cli/github/github_engine.py +641 -0
- k_cli/github/local_hub.py +209 -0
- k_cli/github/pr_watcher.py +129 -0
- k_cli/github/trending.py +205 -0
- k_cli/tools/__init__.py +0 -0
- k_cli/tools/audit.py +79 -0
- k_cli/tools/chaos_immunity.py +377 -0
- k_cli/tools/codebase_qa.py +106 -0
- k_cli/tools/command_runner.py +256 -0
- k_cli/tools/diagram_generator.py +547 -0
- k_cli/tools/doc_retriever.py +1332 -0
- k_cli/tools/feature.py +105 -0
- k_cli/tools/ghost_daemon.py +122 -0
- k_cli/tools/incident_triage.py +1365 -0
- k_cli/tools/mcp_client.py +1846 -0
- k_cli/tools/repo_gardener.py +142 -0
- k_cli/tools/rules.py +109 -0
- k_cli/tools/security.py +52 -0
- k_cli/tools/security_healer.py +999 -0
- k_cli/tools/synapse_graph.py +155 -0
- k_cli/tui/__init__.py +0 -0
- k_cli/tui/diff_viewer.py +223 -0
- k_cli/tui/tui.py +1145 -0
- k_cli/tui/tui_animations.py +648 -0
- k_cli/tui/tui_app.py +2788 -0
- k_cli/ui/__init__.py +10 -0
- k_cli/ui/simple_repl.py +315 -0
- k_cli/web/__init__.py +7 -0
- k_cli/web/server.py +624 -0
- k_cli/web/static/app.js +830 -0
- k_cli/web/static/index.html +495 -0
- k_cli/web/static/monitor.html +189 -0
- k_cli/web/static/style.css +838 -0
- k_cli_for_devs-1.0.0.dist-info/METADATA +461 -0
- k_cli_for_devs-1.0.0.dist-info/RECORD +75 -0
- k_cli_for_devs-1.0.0.dist-info/WHEEL +5 -0
- k_cli_for_devs-1.0.0.dist-info/entry_points.txt +2 -0
- k_cli_for_devs-1.0.0.dist-info/licenses/LICENSE +21 -0
- k_cli_for_devs-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
model_manager.py - Model Bootstrapper & Auto-Sync Engine for K-CLI (Project Bankai)
|
|
3
|
+
|
|
4
|
+
Manages automated model lifecycle:
|
|
5
|
+
1. Pulls quantized Bankai GGUF models (e.g. krishivjoshi/bankai-7b, krishivjoshi/bankai-10b)
|
|
6
|
+
from Hugging Face Hub directly into Ollama or local GGUF cache (~/.kcli/models, ~/models).
|
|
7
|
+
2. Verifies SHA256 cryptographic integrity of downloaded GGUF binaries.
|
|
8
|
+
3. Checks local Ollama instance health and auto-creates Ollama models via Modelfiles.
|
|
9
|
+
4. Seamlessly integrates with K-CLI LLM Driver and Typer CLI interface.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
import shutil
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import urllib.error
|
|
22
|
+
import urllib.parse
|
|
23
|
+
import urllib.request
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("k_cli.model_manager")
|
|
29
|
+
|
|
30
|
+
# ------------------------------------------------------------------------------
|
|
31
|
+
# Constants & Defaults
|
|
32
|
+
# ------------------------------------------------------------------------------
|
|
33
|
+
DEFAULT_OLLAMA_URL = "http://localhost:11434"
|
|
34
|
+
DEFAULT_KCLI_DIR = Path.home() / ".kcli"
|
|
35
|
+
DEFAULT_MODELS_DIR = DEFAULT_KCLI_DIR / "models"
|
|
36
|
+
FALLBACK_MODELS_DIR = Path.home() / "models"
|
|
37
|
+
|
|
38
|
+
# Standard ChatML Prompt Template for Bankai Qwen-based models
|
|
39
|
+
DEFAULT_CHATML_TEMPLATE = """<|im_start|>system
|
|
40
|
+
{{ .System }}<|im_end|>
|
|
41
|
+
<|im_start|>user
|
|
42
|
+
{{ .Prompt }}<|im_end|>
|
|
43
|
+
<|im_start|>assistant
|
|
44
|
+
{{ .Response }}<|im_end|>
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
# Preset Model Catalog
|
|
48
|
+
MODEL_CATALOG: Dict[str, Dict[str, Any]] = {
|
|
49
|
+
"bankai-7b": {
|
|
50
|
+
"repo_id": "krishivjoshi/bankai-7b",
|
|
51
|
+
"default_filename": "bankai-7b.gguf",
|
|
52
|
+
"candidate_filenames": [
|
|
53
|
+
"bankai-7b.gguf",
|
|
54
|
+
"bankai-7b-q4_k_m.gguf",
|
|
55
|
+
"bankai-7b.Q4_K_M.gguf",
|
|
56
|
+
"qwen2.5-coder-7b-instruct.Q4_K_M.gguf",
|
|
57
|
+
],
|
|
58
|
+
"ollama_tag": "bankai:7b",
|
|
59
|
+
"aliases": ["bankai-7b", "bankai:7b", "7b", "krishivjoshi/bankai-7b"],
|
|
60
|
+
"system_prompt": (
|
|
61
|
+
"You are Bankai-7B, an elite compiler-grounded AI coding model operating under strict "
|
|
62
|
+
"1.0 GB RAM constraints. You specialize in complex AST code generation, surgical "
|
|
63
|
+
"SEARCH/REPLACE patches, zero-fluff critique, and step-by-step technical reasoning inside <think>...</think> tags."
|
|
64
|
+
),
|
|
65
|
+
"temperature": 0.2,
|
|
66
|
+
"top_p": 0.95,
|
|
67
|
+
"repeat_penalty": 1.1,
|
|
68
|
+
"stop_tokens": ["<|im_start|>", "<|im_end|>"],
|
|
69
|
+
},
|
|
70
|
+
"bankai-10b": {
|
|
71
|
+
"repo_id": "krishivjoshi/bankai-10b",
|
|
72
|
+
"default_filename": "bankai-10b.gguf",
|
|
73
|
+
"candidate_filenames": [
|
|
74
|
+
"bankai-10b.gguf",
|
|
75
|
+
"bankai-10b-q4_k_m.gguf",
|
|
76
|
+
"bankai-10b.Q4_K_M.gguf",
|
|
77
|
+
"qwen2.5-coder-10b-instruct.Q4_K_M.gguf",
|
|
78
|
+
],
|
|
79
|
+
"ollama_tag": "bankai:10b",
|
|
80
|
+
"aliases": ["bankai-10b", "bankai:10b", "10b", "krishivjoshi/bankai-10b"],
|
|
81
|
+
"system_prompt": (
|
|
82
|
+
"You are Bankai-10B, a high-capacity compiler-grounded AI coding model operating under strict "
|
|
83
|
+
"RAM constraints. You specialize in multi-file architecture reasoning, surgical AST patches, "
|
|
84
|
+
"and formal verification inside <think>...</think> tags."
|
|
85
|
+
),
|
|
86
|
+
"temperature": 0.2,
|
|
87
|
+
"top_p": 0.95,
|
|
88
|
+
"repeat_penalty": 1.1,
|
|
89
|
+
"stop_tokens": ["<|im_start|>", "<|im_end|>"],
|
|
90
|
+
},
|
|
91
|
+
"bankai-1.5b": {
|
|
92
|
+
"repo_id": "krishivjoshi/bankai-1.5b",
|
|
93
|
+
"default_filename": "bankai-1.5b.gguf",
|
|
94
|
+
"candidate_filenames": [
|
|
95
|
+
"bankai-1.5b.gguf",
|
|
96
|
+
"bankai-1.5b-q4_k_m.gguf",
|
|
97
|
+
"qwen2.5-coder-1.5b-instruct.Q4_K_M.gguf",
|
|
98
|
+
],
|
|
99
|
+
"ollama_tag": "bankai:1.5b",
|
|
100
|
+
"aliases": ["bankai-1.5b", "bankai:1.5b", "1.5b", "krishivjoshi/bankai-1.5b"],
|
|
101
|
+
"system_prompt": (
|
|
102
|
+
"You are Bankai-1.5B, a fast lightweight compiler-grounded AI coding model operating under strict "
|
|
103
|
+
"1.0 GB RAM constraints. You specialize in AST code generation and surgical patches inside <think>...</think> tags."
|
|
104
|
+
),
|
|
105
|
+
"temperature": 0.2,
|
|
106
|
+
"top_p": 0.95,
|
|
107
|
+
"repeat_penalty": 1.1,
|
|
108
|
+
"stop_tokens": ["<|im_start|>", "<|im_end|>"],
|
|
109
|
+
},
|
|
110
|
+
"bankai-3b": {
|
|
111
|
+
"repo_id": "krishivjoshi/bankai-3b",
|
|
112
|
+
"default_filename": "bankai-3b.gguf",
|
|
113
|
+
"candidate_filenames": [
|
|
114
|
+
"bankai-3b.gguf",
|
|
115
|
+
"bankai-3b-q4_k_m.gguf",
|
|
116
|
+
"qwen2.5-coder-3b-instruct.Q4_K_M.gguf",
|
|
117
|
+
],
|
|
118
|
+
"ollama_tag": "bankai:3b",
|
|
119
|
+
"aliases": ["bankai-3b", "bankai:3b", "3b", "krishivjoshi/bankai-3b"],
|
|
120
|
+
"system_prompt": (
|
|
121
|
+
"You are Bankai-3B, an elite compiler-grounded AI coding model operating under strict "
|
|
122
|
+
"1.0 GB RAM constraints. You specialize in unpadded code generation, surgical SEARCH/REPLACE patches, "
|
|
123
|
+
"AST syntax validation, and step-by-step technical reasoning inside <think>...</think> tags."
|
|
124
|
+
),
|
|
125
|
+
"temperature": 0.2,
|
|
126
|
+
"top_p": 0.95,
|
|
127
|
+
"repeat_penalty": 1.1,
|
|
128
|
+
"stop_tokens": ["<|im_start|>", "<|im_end|>"],
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class ModelPullResult:
|
|
135
|
+
"""Structured result from a model pull operation."""
|
|
136
|
+
success: bool
|
|
137
|
+
model_name: str
|
|
138
|
+
ollama_tag: str
|
|
139
|
+
gguf_path: Optional[Path] = None
|
|
140
|
+
modelfile_path: Optional[Path] = None
|
|
141
|
+
sha256: Optional[str] = None
|
|
142
|
+
sha256_verified: bool = False
|
|
143
|
+
ollama_created: bool = False
|
|
144
|
+
ollama_healthy: bool = False
|
|
145
|
+
message: str = ""
|
|
146
|
+
details: Dict[str, Any] = field(default_factory=dict)
|
|
147
|
+
|
|
148
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
149
|
+
return {
|
|
150
|
+
"success": self.success,
|
|
151
|
+
"model_name": self.model_name,
|
|
152
|
+
"ollama_tag": self.ollama_tag,
|
|
153
|
+
"gguf_path": str(self.gguf_path) if self.gguf_path else None,
|
|
154
|
+
"modelfile_path": str(self.modelfile_path) if self.modelfile_path else None,
|
|
155
|
+
"sha256": self.sha256,
|
|
156
|
+
"sha256_verified": self.sha256_verified,
|
|
157
|
+
"ollama_created": self.ollama_created,
|
|
158
|
+
"ollama_healthy": self.ollama_healthy,
|
|
159
|
+
"message": self.message,
|
|
160
|
+
"details": self.details,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class ModelManager:
|
|
165
|
+
"""
|
|
166
|
+
Automated Model Lifecycle, GGUF Cache, Integrity Verification, and Ollama Registration Engine.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
def __init__(
|
|
170
|
+
self,
|
|
171
|
+
models_dir: Optional[Union[str, Path]] = None,
|
|
172
|
+
ollama_url: Optional[str] = None,
|
|
173
|
+
mock_mode: Optional[bool] = None,
|
|
174
|
+
):
|
|
175
|
+
if models_dir is not None:
|
|
176
|
+
self.models_dir = Path(models_dir).resolve()
|
|
177
|
+
else:
|
|
178
|
+
self.models_dir = DEFAULT_MODELS_DIR
|
|
179
|
+
|
|
180
|
+
# Ensure model cache directory exists
|
|
181
|
+
self.models_dir.mkdir(parents=True, exist_ok=True)
|
|
182
|
+
|
|
183
|
+
env_ollama = os.getenv("OLLAMA_HOST")
|
|
184
|
+
self.ollama_url = (ollama_url or env_ollama or DEFAULT_OLLAMA_URL).rstrip("/")
|
|
185
|
+
if not self.ollama_url.startswith("http"):
|
|
186
|
+
self.ollama_url = f"http://{self.ollama_url}"
|
|
187
|
+
|
|
188
|
+
if mock_mode is not None:
|
|
189
|
+
self.mock_mode = bool(mock_mode)
|
|
190
|
+
else:
|
|
191
|
+
self.mock_mode = os.getenv("KCLI_MOCK_MODE", "").lower() in ("true", "1") or (
|
|
192
|
+
"PYTEST_CURRENT_TEST" in os.environ and not os.getenv("K_CLI_REAL_LLM")
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Internal mock state for testing
|
|
196
|
+
self._mock_ollama_models: List[str] = ["qwen2.5-coder:1.5b", "bankai:1.5b"]
|
|
197
|
+
|
|
198
|
+
# --------------------------------------------------------------------------
|
|
199
|
+
# 1. Model Resolution & Spec Lookup
|
|
200
|
+
# --------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def resolve_model_spec(model_identifier: str) -> Dict[str, Any]:
|
|
204
|
+
"""
|
|
205
|
+
Resolves model metadata and parameters for a given name, alias, or HF repo.
|
|
206
|
+
"""
|
|
207
|
+
clean_id = model_identifier.strip().lower()
|
|
208
|
+
|
|
209
|
+
# Exact key or alias match
|
|
210
|
+
for key, spec in MODEL_CATALOG.items():
|
|
211
|
+
if clean_id == key or clean_id in spec.get("aliases", []):
|
|
212
|
+
return dict(spec)
|
|
213
|
+
|
|
214
|
+
# Partial matching (e.g. '7b' -> 'bankai-7b')
|
|
215
|
+
for key, spec in MODEL_CATALOG.items():
|
|
216
|
+
if clean_id in key or any(clean_id == a.lower() for a in spec.get("aliases", [])):
|
|
217
|
+
return dict(spec)
|
|
218
|
+
|
|
219
|
+
# Fallback for custom Hugging Face repository or custom model tag
|
|
220
|
+
repo_id = model_identifier if "/" in model_identifier else f"krishivjoshi/{model_identifier}"
|
|
221
|
+
clean_name = model_identifier.split("/")[-1].replace(":", "-")
|
|
222
|
+
tag_name = model_identifier.split("/")[-1].replace("-", ":")
|
|
223
|
+
return {
|
|
224
|
+
"repo_id": repo_id,
|
|
225
|
+
"default_filename": f"{clean_name}.gguf",
|
|
226
|
+
"candidate_filenames": [f"{clean_name}.gguf", f"{clean_name}-q4_k_m.gguf", f"{clean_name}.Q4_K_M.gguf"],
|
|
227
|
+
"ollama_tag": tag_name,
|
|
228
|
+
"aliases": [model_identifier, clean_name, tag_name],
|
|
229
|
+
"system_prompt": (
|
|
230
|
+
f"You are {clean_name.upper()}, an elite compiler-grounded AI coding model operating under "
|
|
231
|
+
"strict RAM constraints. You specialize in AST code generation and surgical patches inside <think>...</think> tags."
|
|
232
|
+
),
|
|
233
|
+
"temperature": 0.2,
|
|
234
|
+
"top_p": 0.95,
|
|
235
|
+
"repeat_penalty": 1.1,
|
|
236
|
+
"stop_tokens": ["<|im_start|>", "<|im_end|>"],
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# --------------------------------------------------------------------------
|
|
240
|
+
# 2. Ollama Health & Model Registration
|
|
241
|
+
# --------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
def check_ollama_health(self) -> Dict[str, Any]:
|
|
244
|
+
"""
|
|
245
|
+
Checks local Ollama service availability, version, and loaded models.
|
|
246
|
+
"""
|
|
247
|
+
if self.mock_mode:
|
|
248
|
+
return {
|
|
249
|
+
"healthy": True,
|
|
250
|
+
"version": "0.1.0-mock",
|
|
251
|
+
"models": list(self._mock_ollama_models),
|
|
252
|
+
"url": self.ollama_url,
|
|
253
|
+
"error": None,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
health_data: Dict[str, Any] = {
|
|
257
|
+
"healthy": False,
|
|
258
|
+
"version": "unknown",
|
|
259
|
+
"models": [],
|
|
260
|
+
"url": self.ollama_url,
|
|
261
|
+
"error": None,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
# 1. Check /api/tags
|
|
265
|
+
try:
|
|
266
|
+
req = urllib.request.Request(f"{self.ollama_url}/api/tags", method="GET")
|
|
267
|
+
with urllib.request.urlopen(req, timeout=3.0) as resp:
|
|
268
|
+
if resp.status == 200:
|
|
269
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
270
|
+
models = [m.get("name", "") for m in data.get("models", []) if isinstance(m, dict)]
|
|
271
|
+
health_data["healthy"] = True
|
|
272
|
+
health_data["models"] = [m for m in models if m]
|
|
273
|
+
except Exception as e:
|
|
274
|
+
health_data["error"] = str(e)
|
|
275
|
+
return health_data
|
|
276
|
+
|
|
277
|
+
# 2. Check /api/version if available
|
|
278
|
+
try:
|
|
279
|
+
req_ver = urllib.request.Request(f"{self.ollama_url}/api/version", method="GET")
|
|
280
|
+
with urllib.request.urlopen(req_ver, timeout=2.0) as resp:
|
|
281
|
+
if resp.status == 200:
|
|
282
|
+
v_data = json.loads(resp.read().decode("utf-8"))
|
|
283
|
+
health_data["version"] = v_data.get("version", "unknown")
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
return health_data
|
|
288
|
+
|
|
289
|
+
def is_ollama_available(self) -> bool:
|
|
290
|
+
"""Returns True if local Ollama daemon is active and responsive."""
|
|
291
|
+
return self.check_ollama_health().get("healthy", False)
|
|
292
|
+
|
|
293
|
+
def list_ollama_models(self) -> List[str]:
|
|
294
|
+
"""Returns list of all model tags available in Ollama."""
|
|
295
|
+
return self.check_ollama_health().get("models", [])
|
|
296
|
+
|
|
297
|
+
def has_ollama_model(self, model_tag: str) -> bool:
|
|
298
|
+
"""Checks whether a specific model tag exists in Ollama."""
|
|
299
|
+
models = self.list_ollama_models()
|
|
300
|
+
target = model_tag.strip().lower()
|
|
301
|
+
for m in models:
|
|
302
|
+
m_low = m.lower()
|
|
303
|
+
if target == m_low or m_low.startswith(target) or target.startswith(m_low.split(":")[0]):
|
|
304
|
+
return True
|
|
305
|
+
# Also check without tag suffix (e.g. bankai-7b vs bankai:7b)
|
|
306
|
+
norm_target = target.replace("-", ":")
|
|
307
|
+
norm_m = m_low.replace("-", ":")
|
|
308
|
+
if norm_target == norm_m or norm_target.split(":")[0] == norm_m.split(":")[0]:
|
|
309
|
+
if ":" in target and ":" in m_low and target.split(":")[-1] == m_low.split(":")[-1]:
|
|
310
|
+
return True
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
# --------------------------------------------------------------------------
|
|
314
|
+
# 3. Cryptographic SHA256 Integrity Verification
|
|
315
|
+
# --------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
@staticmethod
|
|
318
|
+
def compute_sha256(
|
|
319
|
+
file_path: Union[str, Path],
|
|
320
|
+
chunk_size: int = 1024 * 1024 * 4, # 4MB chunks
|
|
321
|
+
progress_callback: Optional[Callable[[int, int], None]] = None,
|
|
322
|
+
) -> str:
|
|
323
|
+
"""
|
|
324
|
+
Computes SHA256 hash of a local binary file.
|
|
325
|
+
"""
|
|
326
|
+
fp = Path(file_path)
|
|
327
|
+
if not fp.exists() or not fp.is_file():
|
|
328
|
+
raise FileNotFoundError(f"Cannot compute SHA256: file '{fp}' not found.")
|
|
329
|
+
|
|
330
|
+
total_size = fp.stat().st_size
|
|
331
|
+
bytes_read = 0
|
|
332
|
+
sha256_hasher = hashlib.sha256()
|
|
333
|
+
|
|
334
|
+
with open(fp, "rb") as f:
|
|
335
|
+
while True:
|
|
336
|
+
chunk = f.read(chunk_size)
|
|
337
|
+
if not chunk:
|
|
338
|
+
break
|
|
339
|
+
sha256_hasher.update(chunk)
|
|
340
|
+
bytes_read += len(chunk)
|
|
341
|
+
if progress_callback:
|
|
342
|
+
progress_callback(bytes_read, total_size)
|
|
343
|
+
|
|
344
|
+
return sha256_hasher.hexdigest().lower()
|
|
345
|
+
|
|
346
|
+
@classmethod
|
|
347
|
+
def verify_sha256(
|
|
348
|
+
cls,
|
|
349
|
+
file_path: Union[str, Path],
|
|
350
|
+
expected_sha256: str,
|
|
351
|
+
chunk_size: int = 1024 * 1024 * 4,
|
|
352
|
+
) -> bool:
|
|
353
|
+
"""
|
|
354
|
+
Validates whether file matches expected SHA256 digest.
|
|
355
|
+
"""
|
|
356
|
+
if not expected_sha256 or not expected_sha256.strip():
|
|
357
|
+
return False
|
|
358
|
+
calculated = cls.compute_sha256(file_path, chunk_size=chunk_size)
|
|
359
|
+
return calculated.lower() == expected_sha256.strip().lower()
|
|
360
|
+
|
|
361
|
+
def fetch_hf_metadata(
|
|
362
|
+
self,
|
|
363
|
+
repo_id: str,
|
|
364
|
+
filename: Optional[str] = None,
|
|
365
|
+
hf_token: Optional[str] = None,
|
|
366
|
+
) -> Dict[str, Any]:
|
|
367
|
+
"""
|
|
368
|
+
Queries Hugging Face API to extract remote file list and LFS SHA256 metadata.
|
|
369
|
+
"""
|
|
370
|
+
if self.mock_mode:
|
|
371
|
+
return {
|
|
372
|
+
"repo_id": repo_id,
|
|
373
|
+
"files": [filename or "model.gguf"],
|
|
374
|
+
"lfs_sha256": {"bankai-7b.gguf": "mock_sha256_bankai_7b", "bankai-10b.gguf": "mock_sha256_bankai_10b"},
|
|
375
|
+
"size_map": {filename or "model.gguf": 4000000000},
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
token = hf_token or os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
|
|
379
|
+
meta_url = f"https://huggingface.co/api/models/{repo_id}"
|
|
380
|
+
headers = {"User-Agent": "K-CLI/0.2.0 ModelManager"}
|
|
381
|
+
if token:
|
|
382
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
383
|
+
|
|
384
|
+
try:
|
|
385
|
+
req = urllib.request.Request(meta_url, headers=headers, method="GET")
|
|
386
|
+
with urllib.request.urlopen(req, timeout=10.0) as resp:
|
|
387
|
+
if resp.status == 200:
|
|
388
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
389
|
+
siblings = data.get("siblings", [])
|
|
390
|
+
file_names = [s.get("rfilename", "") for s in siblings if isinstance(s, dict)]
|
|
391
|
+
lfs_map: Dict[str, str] = {}
|
|
392
|
+
size_map: Dict[str, int] = {}
|
|
393
|
+
|
|
394
|
+
# Inspect siblings or tags for LFS hashes
|
|
395
|
+
for s in siblings:
|
|
396
|
+
fn = s.get("rfilename", "")
|
|
397
|
+
lfs = s.get("lfs", {})
|
|
398
|
+
if isinstance(lfs, dict) and "sha256" in lfs:
|
|
399
|
+
lfs_map[fn] = lfs["sha256"]
|
|
400
|
+
if isinstance(lfs, dict) and "size" in lfs:
|
|
401
|
+
size_map[fn] = lfs["size"]
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
"repo_id": repo_id,
|
|
405
|
+
"files": file_names,
|
|
406
|
+
"lfs_sha256": lfs_map,
|
|
407
|
+
"size_map": size_map,
|
|
408
|
+
}
|
|
409
|
+
except Exception as e:
|
|
410
|
+
logger.debug(f"HF API metadata lookup notice for {repo_id}: {e}")
|
|
411
|
+
|
|
412
|
+
return {"repo_id": repo_id, "files": [], "lfs_sha256": {}, "size_map": {}}
|
|
413
|
+
|
|
414
|
+
# --------------------------------------------------------------------------
|
|
415
|
+
# 4. GGUF Location & Hugging Face Hub Pull
|
|
416
|
+
# --------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
def find_local_gguf(self, model_identifier: str) -> Optional[Path]:
|
|
419
|
+
"""
|
|
420
|
+
Searches local storage directories for existing GGUF binaries.
|
|
421
|
+
"""
|
|
422
|
+
spec = self.resolve_model_spec(model_identifier)
|
|
423
|
+
candidates = spec.get("candidate_filenames", [spec.get("default_filename", f"{model_identifier}.gguf")])
|
|
424
|
+
|
|
425
|
+
search_dirs = [
|
|
426
|
+
self.models_dir,
|
|
427
|
+
FALLBACK_MODELS_DIR,
|
|
428
|
+
DEFAULT_KCLI_DIR,
|
|
429
|
+
Path("/content"), # Colab runtime root
|
|
430
|
+
Path("/content/bankai_7b_model"),
|
|
431
|
+
Path("/content/bankai_10b_model"),
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
for s_dir in search_dirs:
|
|
435
|
+
if not s_dir.exists():
|
|
436
|
+
continue
|
|
437
|
+
for cand in candidates:
|
|
438
|
+
p = s_dir / cand
|
|
439
|
+
if p.exists() and p.is_file() and p.stat().st_size > 1024:
|
|
440
|
+
return p
|
|
441
|
+
|
|
442
|
+
# Also check direct glob matches
|
|
443
|
+
clean_name = model_identifier.split("/")[-1].replace(":", "-").lower()
|
|
444
|
+
for f in s_dir.glob("*.gguf"):
|
|
445
|
+
if clean_name in f.name.lower():
|
|
446
|
+
return f
|
|
447
|
+
|
|
448
|
+
return None
|
|
449
|
+
|
|
450
|
+
def download_from_hf(
|
|
451
|
+
self,
|
|
452
|
+
repo_id: str,
|
|
453
|
+
filename: Optional[str] = None,
|
|
454
|
+
target_path: Optional[Path] = None,
|
|
455
|
+
hf_token: Optional[str] = None,
|
|
456
|
+
quant_preference: str = "q4_k_m",
|
|
457
|
+
progress_callback: Optional[Callable[[int, int], None]] = None,
|
|
458
|
+
) -> Path:
|
|
459
|
+
"""
|
|
460
|
+
Downloads GGUF weights directly from Hugging Face Hub.
|
|
461
|
+
"""
|
|
462
|
+
token = hf_token or os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
|
|
463
|
+
|
|
464
|
+
# Resolve candidate filename if not specified
|
|
465
|
+
resolved_filename = filename
|
|
466
|
+
if not resolved_filename:
|
|
467
|
+
# Check model metadata
|
|
468
|
+
meta = self.fetch_hf_metadata(repo_id, hf_token=token)
|
|
469
|
+
files = meta.get("files", [])
|
|
470
|
+
gguf_files = [f for f in files if f.endswith(".gguf")]
|
|
471
|
+
if gguf_files:
|
|
472
|
+
# Prefer requested quantization
|
|
473
|
+
pref = [f for f in gguf_files if quant_preference.lower() in f.lower()]
|
|
474
|
+
resolved_filename = pref[0] if pref else gguf_files[0]
|
|
475
|
+
else:
|
|
476
|
+
spec = self.resolve_model_spec(repo_id)
|
|
477
|
+
resolved_filename = spec.get("default_filename", "model.gguf")
|
|
478
|
+
|
|
479
|
+
# Target destination
|
|
480
|
+
if target_path is not None:
|
|
481
|
+
dest = Path(target_path).resolve()
|
|
482
|
+
else:
|
|
483
|
+
dest = self.models_dir / resolved_filename
|
|
484
|
+
|
|
485
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
486
|
+
|
|
487
|
+
if self.mock_mode:
|
|
488
|
+
# In mock mode, write a lightweight mock binary file
|
|
489
|
+
if not dest.exists():
|
|
490
|
+
mock_content = (
|
|
491
|
+
b"GGUF\x03\x00\x00\x00" # Valid GGUF magic header
|
|
492
|
+
b"\x00\x00\x00\x00\x00\x00\x00\x00"
|
|
493
|
+
+ f"Mock weights for {repo_id}/{resolved_filename}".encode("utf-8")
|
|
494
|
+
)
|
|
495
|
+
dest.write_bytes(mock_content)
|
|
496
|
+
if progress_callback:
|
|
497
|
+
progress_callback(len(dest.read_bytes()), len(dest.read_bytes()))
|
|
498
|
+
return dest
|
|
499
|
+
|
|
500
|
+
# Strategy 1: huggingface_hub library
|
|
501
|
+
try:
|
|
502
|
+
from huggingface_hub import hf_hub_download
|
|
503
|
+
downloaded = hf_hub_download(
|
|
504
|
+
repo_id=repo_id,
|
|
505
|
+
filename=resolved_filename,
|
|
506
|
+
token=token,
|
|
507
|
+
local_dir=str(self.models_dir),
|
|
508
|
+
local_dir_use_symlinks=False,
|
|
509
|
+
)
|
|
510
|
+
downloaded_path = Path(downloaded)
|
|
511
|
+
if downloaded_path.resolve() != dest.resolve():
|
|
512
|
+
shutil.copy2(downloaded_path, dest)
|
|
513
|
+
return dest
|
|
514
|
+
except Exception as e_hf:
|
|
515
|
+
logger.debug(f"huggingface_hub direct download fallback: {e_hf}")
|
|
516
|
+
|
|
517
|
+
# Strategy 2: Direct HTTP Streaming Download
|
|
518
|
+
download_url = f"https://huggingface.co/{repo_id}/resolve/main/{resolved_filename}"
|
|
519
|
+
headers = {"User-Agent": "K-CLI/0.2.0 ModelManager"}
|
|
520
|
+
if token:
|
|
521
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
522
|
+
|
|
523
|
+
req = urllib.request.Request(download_url, headers=headers, method="GET")
|
|
524
|
+
with urllib.request.urlopen(req, timeout=120.0) as resp:
|
|
525
|
+
if resp.status != 200:
|
|
526
|
+
raise RuntimeError(f"Failed to download from HF ({download_url}): HTTP {resp.status}")
|
|
527
|
+
|
|
528
|
+
total_bytes = int(resp.headers.get("content-length", 0))
|
|
529
|
+
downloaded_bytes = 0
|
|
530
|
+
|
|
531
|
+
temp_dest = dest.with_suffix(".tmp")
|
|
532
|
+
with open(temp_dest, "wb") as f_out:
|
|
533
|
+
while True:
|
|
534
|
+
chunk = resp.read(1024 * 1024 * 4) # 4MB chunks
|
|
535
|
+
if not chunk:
|
|
536
|
+
break
|
|
537
|
+
f_out.write(chunk)
|
|
538
|
+
downloaded_bytes += len(chunk)
|
|
539
|
+
if progress_callback:
|
|
540
|
+
progress_callback(downloaded_bytes, total_bytes)
|
|
541
|
+
|
|
542
|
+
temp_dest.replace(dest)
|
|
543
|
+
|
|
544
|
+
return dest
|
|
545
|
+
|
|
546
|
+
# --------------------------------------------------------------------------
|
|
547
|
+
# 5. Modelfile Generation & Ollama Deployment
|
|
548
|
+
# --------------------------------------------------------------------------
|
|
549
|
+
|
|
550
|
+
def generate_modelfile(
|
|
551
|
+
self,
|
|
552
|
+
gguf_path: Union[str, Path],
|
|
553
|
+
model_name: str = "bankai-7b",
|
|
554
|
+
system_prompt: Optional[str] = None,
|
|
555
|
+
template: Optional[str] = None,
|
|
556
|
+
temperature: Optional[float] = None,
|
|
557
|
+
top_p: Optional[float] = None,
|
|
558
|
+
repeat_penalty: Optional[float] = None,
|
|
559
|
+
) -> str:
|
|
560
|
+
"""
|
|
561
|
+
Generates standard Modelfile content for Ollama model creation.
|
|
562
|
+
"""
|
|
563
|
+
spec = self.resolve_model_spec(model_name)
|
|
564
|
+
gguf_abs = Path(gguf_path).resolve()
|
|
565
|
+
|
|
566
|
+
sys_prompt = system_prompt or spec.get("system_prompt", "")
|
|
567
|
+
tmpl = template or DEFAULT_CHATML_TEMPLATE
|
|
568
|
+
temp = temperature if temperature is not None else spec.get("temperature", 0.2)
|
|
569
|
+
tp = top_p if top_p is not None else spec.get("top_p", 0.95)
|
|
570
|
+
rp = repeat_penalty if repeat_penalty is not None else spec.get("repeat_penalty", 1.1)
|
|
571
|
+
stop_tokens = spec.get("stop_tokens", ["<|im_start|>", "<|im_end|>"])
|
|
572
|
+
|
|
573
|
+
lines = [
|
|
574
|
+
f"FROM {gguf_abs}",
|
|
575
|
+
"",
|
|
576
|
+
'TEMPLATE """' + tmpl.strip() + '"""',
|
|
577
|
+
"",
|
|
578
|
+
]
|
|
579
|
+
|
|
580
|
+
for st in stop_tokens:
|
|
581
|
+
lines.append(f'PARAMETER stop "{st}"')
|
|
582
|
+
|
|
583
|
+
lines.append(f"PARAMETER temperature {temp}")
|
|
584
|
+
lines.append(f"PARAMETER top_p {tp}")
|
|
585
|
+
lines.append(f"PARAMETER repeat_penalty {rp}")
|
|
586
|
+
lines.append("")
|
|
587
|
+
lines.append(f'SYSTEM """{sys_prompt.strip()}"""')
|
|
588
|
+
lines.append("")
|
|
589
|
+
|
|
590
|
+
return "\n".join(lines)
|
|
591
|
+
|
|
592
|
+
def write_modelfile(
|
|
593
|
+
self,
|
|
594
|
+
gguf_path: Union[str, Path],
|
|
595
|
+
output_modelfile_path: Optional[Union[str, Path]] = None,
|
|
596
|
+
model_name: str = "bankai-7b",
|
|
597
|
+
system_prompt: Optional[str] = None,
|
|
598
|
+
) -> Path:
|
|
599
|
+
"""
|
|
600
|
+
Writes generated Modelfile to disk and returns path.
|
|
601
|
+
"""
|
|
602
|
+
content = self.generate_modelfile(
|
|
603
|
+
gguf_path=gguf_path,
|
|
604
|
+
model_name=model_name,
|
|
605
|
+
system_prompt=system_prompt,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
if output_modelfile_path is not None:
|
|
609
|
+
out_p = Path(output_modelfile_path).resolve()
|
|
610
|
+
else:
|
|
611
|
+
clean_name = model_name.replace(":", "-").replace("/", "-")
|
|
612
|
+
out_p = self.models_dir / f"Modelfile.{clean_name}"
|
|
613
|
+
|
|
614
|
+
out_p.parent.mkdir(parents=True, exist_ok=True)
|
|
615
|
+
out_p.write_text(content, encoding="utf-8")
|
|
616
|
+
return out_p
|
|
617
|
+
|
|
618
|
+
def create_ollama_model(
|
|
619
|
+
self,
|
|
620
|
+
model_tag: str,
|
|
621
|
+
modelfile_path: Union[str, Path],
|
|
622
|
+
) -> Tuple[bool, str]:
|
|
623
|
+
"""
|
|
624
|
+
Registers model with local Ollama service using 'ollama create' CLI or API.
|
|
625
|
+
"""
|
|
626
|
+
mf_path = Path(modelfile_path).resolve()
|
|
627
|
+
if not mf_path.exists():
|
|
628
|
+
return False, f"Modelfile not found at '{mf_path}'."
|
|
629
|
+
|
|
630
|
+
if self.mock_mode:
|
|
631
|
+
if model_tag not in self._mock_ollama_models:
|
|
632
|
+
self._mock_ollama_models.append(model_tag)
|
|
633
|
+
return True, f"Mock: Successfully created Ollama model '{model_tag}' from {mf_path.name}"
|
|
634
|
+
|
|
635
|
+
# Strategy 1: Ollama CLI (`ollama create <tag> -f <modelfile>`)
|
|
636
|
+
ollama_bin = shutil.which("ollama") or "/home/k/bin/ollama"
|
|
637
|
+
if os.path.exists(ollama_bin) and os.access(ollama_bin, os.X_OK):
|
|
638
|
+
try:
|
|
639
|
+
cmd = [ollama_bin, "create", model_tag, "-f", str(mf_path)]
|
|
640
|
+
res = subprocess.run(
|
|
641
|
+
cmd,
|
|
642
|
+
capture_output=True,
|
|
643
|
+
text=True,
|
|
644
|
+
timeout=300,
|
|
645
|
+
)
|
|
646
|
+
if res.returncode == 0:
|
|
647
|
+
return True, f"Successfully created Ollama model '{model_tag}' via CLI."
|
|
648
|
+
else:
|
|
649
|
+
logger.debug(f"Ollama CLI create failed (code {res.returncode}): {res.stderr}")
|
|
650
|
+
except Exception as e_cli:
|
|
651
|
+
logger.debug(f"Ollama CLI invocation error: {e_cli}")
|
|
652
|
+
|
|
653
|
+
# Strategy 2: Ollama HTTP REST API (`/api/create`)
|
|
654
|
+
try:
|
|
655
|
+
create_url = f"{self.ollama_url}/api/create"
|
|
656
|
+
modelfile_content = mf_path.read_text(encoding="utf-8")
|
|
657
|
+
payload = {
|
|
658
|
+
"name": model_tag,
|
|
659
|
+
"modelfile": modelfile_content,
|
|
660
|
+
"stream": False,
|
|
661
|
+
}
|
|
662
|
+
data = json.dumps(payload).encode("utf-8")
|
|
663
|
+
req = urllib.request.Request(
|
|
664
|
+
create_url,
|
|
665
|
+
data=data,
|
|
666
|
+
headers={"Content-Type": "application/json"},
|
|
667
|
+
method="POST",
|
|
668
|
+
)
|
|
669
|
+
with urllib.request.urlopen(req, timeout=300) as resp:
|
|
670
|
+
if resp.status == 200:
|
|
671
|
+
return True, f"Successfully created Ollama model '{model_tag}' via API."
|
|
672
|
+
return False, f"Ollama API returned HTTP {resp.status} during model creation."
|
|
673
|
+
except Exception as e_api:
|
|
674
|
+
return False, f"Failed to create Ollama model '{model_tag}': {e_api}"
|
|
675
|
+
|
|
676
|
+
# --------------------------------------------------------------------------
|
|
677
|
+
# 6. High-Level Automated Model Pull & Bootstrapping Pipeline
|
|
678
|
+
# --------------------------------------------------------------------------
|
|
679
|
+
|
|
680
|
+
def pull_model(
|
|
681
|
+
self,
|
|
682
|
+
model_identifier: str = "bankai-7b",
|
|
683
|
+
ollama_tag: Optional[str] = None,
|
|
684
|
+
hf_repo: Optional[str] = None,
|
|
685
|
+
force: bool = False,
|
|
686
|
+
verify_sha: bool = True,
|
|
687
|
+
create_in_ollama: bool = True,
|
|
688
|
+
expected_sha256: Optional[str] = None,
|
|
689
|
+
quant: str = "q4_k_m",
|
|
690
|
+
progress_callback: Optional[Callable[[int, int], None]] = None,
|
|
691
|
+
) -> ModelPullResult:
|
|
692
|
+
"""
|
|
693
|
+
Executes end-to-end model synchronization:
|
|
694
|
+
1. Resolves Hugging Face repo & target model spec.
|
|
695
|
+
2. Checks local cache or downloads GGUF weights directly from Hugging Face Hub.
|
|
696
|
+
3. Computes and verifies SHA256 integrity against expected or remote LFS hash.
|
|
697
|
+
4. Generates Modelfile and registers the model in Ollama.
|
|
698
|
+
"""
|
|
699
|
+
spec = self.resolve_model_spec(model_identifier)
|
|
700
|
+
repo_id = hf_repo or spec.get("repo_id", f"krishivjoshi/{model_identifier}")
|
|
701
|
+
tag = ollama_tag or spec.get("ollama_tag", model_identifier)
|
|
702
|
+
|
|
703
|
+
# Check local cache first
|
|
704
|
+
local_gguf = self.find_local_gguf(model_identifier) if not force else None
|
|
705
|
+
downloaded = False
|
|
706
|
+
|
|
707
|
+
if local_gguf and local_gguf.exists() and not force:
|
|
708
|
+
gguf_path = local_gguf
|
|
709
|
+
logger.info(f"Using cached GGUF artifact: {gguf_path}")
|
|
710
|
+
else:
|
|
711
|
+
try:
|
|
712
|
+
gguf_path = self.download_from_hf(
|
|
713
|
+
repo_id=repo_id,
|
|
714
|
+
quant_preference=quant,
|
|
715
|
+
progress_callback=progress_callback,
|
|
716
|
+
)
|
|
717
|
+
downloaded = True
|
|
718
|
+
except Exception as dl_err:
|
|
719
|
+
return ModelPullResult(
|
|
720
|
+
success=False,
|
|
721
|
+
model_name=model_identifier,
|
|
722
|
+
ollama_tag=tag,
|
|
723
|
+
message=f"Failed to download '{repo_id}' from Hugging Face Hub: {dl_err}",
|
|
724
|
+
details={"error": str(dl_err)},
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
# Compute SHA256 hash
|
|
728
|
+
computed_sha = None
|
|
729
|
+
sha_verified = False
|
|
730
|
+
try:
|
|
731
|
+
computed_sha = self.compute_sha256(gguf_path)
|
|
732
|
+
target_sha = expected_sha256
|
|
733
|
+
if not target_sha:
|
|
734
|
+
# Attempt to query HF metadata for LFS hash
|
|
735
|
+
meta = self.fetch_hf_metadata(repo_id)
|
|
736
|
+
target_sha = meta.get("lfs_sha256", {}).get(gguf_path.name)
|
|
737
|
+
|
|
738
|
+
if target_sha:
|
|
739
|
+
sha_verified = (computed_sha.lower() == target_sha.strip().lower())
|
|
740
|
+
if verify_sha and not sha_verified:
|
|
741
|
+
return ModelPullResult(
|
|
742
|
+
success=False,
|
|
743
|
+
model_name=model_identifier,
|
|
744
|
+
ollama_tag=tag,
|
|
745
|
+
gguf_path=gguf_path,
|
|
746
|
+
sha256=computed_sha,
|
|
747
|
+
sha256_verified=False,
|
|
748
|
+
message=(
|
|
749
|
+
f"SHA256 integrity mismatch for '{gguf_path.name}'. "
|
|
750
|
+
f"Expected {target_sha}, got {computed_sha}"
|
|
751
|
+
),
|
|
752
|
+
details={"expected_sha": target_sha, "computed_sha": computed_sha},
|
|
753
|
+
)
|
|
754
|
+
else:
|
|
755
|
+
if verify_sha and not self.mock_mode:
|
|
756
|
+
return ModelPullResult(
|
|
757
|
+
success=False,
|
|
758
|
+
model_name=model_identifier,
|
|
759
|
+
ollama_tag=tag,
|
|
760
|
+
gguf_path=gguf_path,
|
|
761
|
+
sha256=computed_sha,
|
|
762
|
+
sha256_verified=False,
|
|
763
|
+
message=(
|
|
764
|
+
f"No trusted SHA256 reference is available for '{gguf_path.name}'. "
|
|
765
|
+
"Refusing to install an unverified model."
|
|
766
|
+
),
|
|
767
|
+
details={"computed_sha": computed_sha},
|
|
768
|
+
)
|
|
769
|
+
sha_verified = self.mock_mode
|
|
770
|
+
except Exception as sha_err:
|
|
771
|
+
logger.warning(f"SHA256 verification warning: {sha_err}")
|
|
772
|
+
|
|
773
|
+
# Generate Modelfile
|
|
774
|
+
modelfile_path = self.write_modelfile(
|
|
775
|
+
gguf_path=gguf_path,
|
|
776
|
+
model_name=model_identifier,
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
# Ollama Health & Model Registration
|
|
780
|
+
ollama_healthy = self.is_ollama_available()
|
|
781
|
+
ollama_created = False
|
|
782
|
+
create_msg = ""
|
|
783
|
+
|
|
784
|
+
if create_in_ollama:
|
|
785
|
+
if ollama_healthy:
|
|
786
|
+
ollama_created, create_msg = self.create_ollama_model(
|
|
787
|
+
model_tag=tag,
|
|
788
|
+
modelfile_path=modelfile_path,
|
|
789
|
+
)
|
|
790
|
+
else:
|
|
791
|
+
create_msg = f"Ollama daemon not reachable at {self.ollama_url}. Modelfile ready at {modelfile_path}."
|
|
792
|
+
|
|
793
|
+
success = (gguf_path.exists() and (not create_in_ollama or ollama_created or not ollama_healthy))
|
|
794
|
+
|
|
795
|
+
return ModelPullResult(
|
|
796
|
+
success=success,
|
|
797
|
+
model_name=model_identifier,
|
|
798
|
+
ollama_tag=tag,
|
|
799
|
+
gguf_path=gguf_path,
|
|
800
|
+
modelfile_path=modelfile_path,
|
|
801
|
+
sha256=computed_sha,
|
|
802
|
+
sha256_verified=sha_verified,
|
|
803
|
+
ollama_created=ollama_created,
|
|
804
|
+
ollama_healthy=ollama_healthy,
|
|
805
|
+
message=create_msg or f"Model '{model_identifier}' successfully staged at {gguf_path}",
|
|
806
|
+
details={
|
|
807
|
+
"downloaded": downloaded,
|
|
808
|
+
"repo_id": repo_id,
|
|
809
|
+
"quant": quant,
|
|
810
|
+
},
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
def init_environment(
|
|
814
|
+
self,
|
|
815
|
+
default_model: str = "bankai-7b",
|
|
816
|
+
sync_model: bool = True,
|
|
817
|
+
force: bool = False,
|
|
818
|
+
) -> Dict[str, Any]:
|
|
819
|
+
"""
|
|
820
|
+
Scaffolds the full K-CLI local directory hierarchy and provisions the default Bankai model.
|
|
821
|
+
"""
|
|
822
|
+
# 1. Scaffolding directory tree
|
|
823
|
+
subdirs = ["docs", "repos", "logs", "khoj_index", "parsers", "models"]
|
|
824
|
+
created_dirs = []
|
|
825
|
+
for sd in subdirs:
|
|
826
|
+
d = DEFAULT_KCLI_DIR / sd
|
|
827
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
828
|
+
created_dirs.append(str(d))
|
|
829
|
+
|
|
830
|
+
# 2. Check Ollama Health
|
|
831
|
+
ollama_status = self.check_ollama_health()
|
|
832
|
+
|
|
833
|
+
# 3. Pull / Sync default Bankai model
|
|
834
|
+
pull_res: Optional[ModelPullResult] = None
|
|
835
|
+
if sync_model:
|
|
836
|
+
pull_res = self.pull_model(
|
|
837
|
+
model_identifier=default_model,
|
|
838
|
+
force=force,
|
|
839
|
+
verify_sha=True,
|
|
840
|
+
create_in_ollama=True,
|
|
841
|
+
)
|
|
842
|
+
|
|
843
|
+
return {
|
|
844
|
+
"kcli_home": str(DEFAULT_KCLI_DIR),
|
|
845
|
+
"directories": created_dirs,
|
|
846
|
+
"ollama": ollama_status,
|
|
847
|
+
"model_pull": pull_res.to_dict() if pull_res else None,
|
|
848
|
+
"ready": True if (not sync_model or (pull_res and pull_res.success)) else False,
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
def pull_ollama_tag(
|
|
852
|
+
self,
|
|
853
|
+
model_tag: str,
|
|
854
|
+
progress_callback: Optional[Callable[[str], None]] = None,
|
|
855
|
+
) -> Tuple[bool, str]:
|
|
856
|
+
"""Pulls a model tag directly from Ollama registry (e.g. qwen2.5-coder:7b)."""
|
|
857
|
+
if self.mock_mode:
|
|
858
|
+
if model_tag not in self._mock_ollama_models:
|
|
859
|
+
self._mock_ollama_models.append(model_tag)
|
|
860
|
+
if progress_callback:
|
|
861
|
+
progress_callback(f"Mock: Pulled {model_tag} 100%")
|
|
862
|
+
return True, f"Mock: Pulled {model_tag} successfully."
|
|
863
|
+
|
|
864
|
+
# Check Ollama CLI
|
|
865
|
+
ollama_bin = shutil.which("ollama") or "/home/k/bin/ollama"
|
|
866
|
+
if os.path.exists(ollama_bin) and os.access(ollama_bin, os.X_OK):
|
|
867
|
+
try:
|
|
868
|
+
proc = subprocess.Popen(
|
|
869
|
+
[ollama_bin, "pull", model_tag],
|
|
870
|
+
stdout=subprocess.PIPE,
|
|
871
|
+
stderr=subprocess.STDOUT,
|
|
872
|
+
text=True,
|
|
873
|
+
)
|
|
874
|
+
if proc.stdout:
|
|
875
|
+
for line in proc.stdout:
|
|
876
|
+
if progress_callback and line.strip():
|
|
877
|
+
progress_callback(line.strip())
|
|
878
|
+
proc.wait(timeout=600)
|
|
879
|
+
if proc.returncode == 0:
|
|
880
|
+
return True, f"Successfully pulled {model_tag} into Ollama."
|
|
881
|
+
except Exception as e:
|
|
882
|
+
logger.debug(f"CLI pull error: {e}")
|
|
883
|
+
|
|
884
|
+
# Fallback to Ollama HTTP API /api/pull
|
|
885
|
+
try:
|
|
886
|
+
req = urllib.request.Request(
|
|
887
|
+
f"{self.ollama_url}/api/pull",
|
|
888
|
+
data=json.dumps({"name": model_tag, "stream": False}).encode("utf-8"),
|
|
889
|
+
headers={"Content-Type": "application/json", "User-Agent": "K-CLI"},
|
|
890
|
+
method="POST",
|
|
891
|
+
)
|
|
892
|
+
with urllib.request.urlopen(req, timeout=600.0) as resp:
|
|
893
|
+
if resp.status == 200:
|
|
894
|
+
return True, f"Successfully pulled {model_tag} via Ollama API."
|
|
895
|
+
except Exception as e:
|
|
896
|
+
return False, f"Failed to pull {model_tag}: {e}"
|
|
897
|
+
|
|
898
|
+
return False, f"Could not pull {model_tag}"
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
# ------------------------------------------------------------------------------
|
|
902
|
+
# Curated Local Coding Models Catalog with In-Depth Pros & Cons
|
|
903
|
+
# ------------------------------------------------------------------------------
|
|
904
|
+
LOCAL_CODING_MODELS: List[Dict[str, Any]] = [
|
|
905
|
+
{
|
|
906
|
+
"id": "qwen2.5-coder:7b",
|
|
907
|
+
"name": "Qwen 2.5 Coder 7B",
|
|
908
|
+
"size": "4.7 GB",
|
|
909
|
+
"ram": "8 GB RAM / 6 GB VRAM",
|
|
910
|
+
"context": "32K / 128K",
|
|
911
|
+
"speed": "~65 tok/s (GPU)",
|
|
912
|
+
"pros": [
|
|
913
|
+
"🏆 #1 Open-Weight Coding Model in 7B class (HumanEval 88.4%)",
|
|
914
|
+
"✨ Flawless Multi-File AST & Surgical SEARCH/REPLACE reasoning",
|
|
915
|
+
"⚡ Fast inference speed with low memory footprint",
|
|
916
|
+
"🌐 Supports 92+ programming languages including Python, Rust, C++, Go",
|
|
917
|
+
],
|
|
918
|
+
"cons": [
|
|
919
|
+
"⚠️ Needs 6GB+ VRAM for full GPU offload",
|
|
920
|
+
"⚠️ May struggle with ultra-large monolithic repos without AST slicing",
|
|
921
|
+
],
|
|
922
|
+
"hf_repo": "Qwen/Qwen2.5-Coder-7B-Instruct-GGUF",
|
|
923
|
+
"ollama_tag": "qwen2.5-coder:7b",
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
"id": "qwen2.5-coder:14b",
|
|
927
|
+
"name": "Qwen 2.5 Coder 14B",
|
|
928
|
+
"size": "9.0 GB",
|
|
929
|
+
"ram": "16 GB RAM / 10 GB VRAM",
|
|
930
|
+
"context": "32K / 128K",
|
|
931
|
+
"speed": "~45 tok/s (GPU)",
|
|
932
|
+
"pros": [
|
|
933
|
+
"🧠 Rivals proprietary GPT-4o-mini and Claude 3.5 Haiku on coding evals",
|
|
934
|
+
"🏗️ Outstanding architectural design and refactoring synthesis",
|
|
935
|
+
"🛡️ High precision on type safety, unit tests, and edge cases",
|
|
936
|
+
],
|
|
937
|
+
"cons": [
|
|
938
|
+
"⚠️ Requires 10GB+ VRAM (or 16GB Mac Unified Memory)",
|
|
939
|
+
"⚠️ Slower execution on pure CPU without AVX-512",
|
|
940
|
+
],
|
|
941
|
+
"hf_repo": "Qwen/Qwen2.5-Coder-14B-Instruct-GGUF",
|
|
942
|
+
"ollama_tag": "qwen2.5-coder:14b",
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
"id": "qwen2.5-coder:1.5b",
|
|
946
|
+
"name": "Qwen 2.5 Coder 1.5B (Ultra-Light)",
|
|
947
|
+
"size": "1.0 GB",
|
|
948
|
+
"ram": "2 GB RAM / 1 GB VRAM",
|
|
949
|
+
"context": "32K",
|
|
950
|
+
"speed": "~140 tok/s (GPU) / ~45 tok/s (CPU)",
|
|
951
|
+
"pros": [
|
|
952
|
+
"🚀 Ultra-fast generation (<10ms time-to-first-token)",
|
|
953
|
+
"💾 Runs on any laptop, Raspberry Pi, or low-spec VM",
|
|
954
|
+
"💰 100% Free, zero CPU throttle, ideal for autocomplete & inline diffs",
|
|
955
|
+
],
|
|
956
|
+
"cons": [
|
|
957
|
+
"⚠️ Limited reasoning depth for complex concurrency & race conditions",
|
|
958
|
+
"⚠️ Lower context capacity for multi-file refactors",
|
|
959
|
+
],
|
|
960
|
+
"hf_repo": "Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF",
|
|
961
|
+
"ollama_tag": "qwen2.5-coder:1.5b",
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
"id": "deepseek-r1:7b",
|
|
965
|
+
"name": "DeepSeek-R1 Distill Qwen 7B (Reasoning)",
|
|
966
|
+
"size": "4.7 GB",
|
|
967
|
+
"ram": "8 GB RAM / 6 GB VRAM",
|
|
968
|
+
"context": "32K / 64K",
|
|
969
|
+
"speed": "~50 tok/s (GPU)",
|
|
970
|
+
"pros": [
|
|
971
|
+
"🧠 Deep Chain-of-Thought reasoning inside visible <think> blocks",
|
|
972
|
+
"🔬 Excellent at finding subtle bugs, race conditions, and cryptographic flaws",
|
|
973
|
+
"⚖️ Self-corrects mistakes before emitting final verified code",
|
|
974
|
+
],
|
|
975
|
+
"cons": [
|
|
976
|
+
"⚠️ Slower total task time due to generating extensive thinking tokens",
|
|
977
|
+
"⚠️ Can overthink trivial tasks like docstrings or variable renames",
|
|
978
|
+
],
|
|
979
|
+
"hf_repo": "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B-GGUF",
|
|
980
|
+
"ollama_tag": "deepseek-r1:7b",
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
"id": "deepseek-r1:14b",
|
|
984
|
+
"name": "DeepSeek-R1 Distill Qwen 14B (Advanced Reasoning)",
|
|
985
|
+
"size": "9.0 GB",
|
|
986
|
+
"ram": "16 GB RAM / 10 GB VRAM",
|
|
987
|
+
"context": "32K / 64K",
|
|
988
|
+
"speed": "~35 tok/s (GPU)",
|
|
989
|
+
"pros": [
|
|
990
|
+
"🎓 Frontier-grade mathematical and algorithmic problem solving",
|
|
991
|
+
"🛡️ Compiler-level verification and multi-step theorem proving",
|
|
992
|
+
"🏆 Top-performing 14B model for complex full-stack architectures",
|
|
993
|
+
],
|
|
994
|
+
"cons": [
|
|
995
|
+
"⚠️ High VRAM requirement (10GB+)",
|
|
996
|
+
"⚠️ Extended latency per response",
|
|
997
|
+
],
|
|
998
|
+
"hf_repo": "deepseek-ai/DeepSeek-R1-Distill-Qwen-14B-GGUF",
|
|
999
|
+
"ollama_tag": "deepseek-r1:14b",
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
"id": "llama3.3:70b",
|
|
1003
|
+
"name": "Meta Llama 3.3 70B (Flagship)",
|
|
1004
|
+
"size": "42 GB (Q4_K_M)",
|
|
1005
|
+
"ram": "48 GB RAM / 40 GB VRAM",
|
|
1006
|
+
"context": "128K",
|
|
1007
|
+
"speed": "~25 tok/s (Multi-GPU)",
|
|
1008
|
+
"pros": [
|
|
1009
|
+
"👑 World-class general knowledge and massive 128k context window",
|
|
1010
|
+
"📚 Flawless technical documentation, architectural synthesis, and RFC generation",
|
|
1011
|
+
"⚡ Matches or exceeds GPT-4-0613 across software engineering evals",
|
|
1012
|
+
],
|
|
1013
|
+
"cons": [
|
|
1014
|
+
"⚠️ Massive hardware requirements (requires dual RTX 3090/4090 or Mac Studio)",
|
|
1015
|
+
"⚠️ Unusable on low-spec single-GPU machines",
|
|
1016
|
+
],
|
|
1017
|
+
"hf_repo": "meta-llama/Llama-3.3-70B-Instruct-GGUF",
|
|
1018
|
+
"ollama_tag": "llama3.3:70b",
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
"id": "phi4:14b",
|
|
1022
|
+
"name": "Microsoft Phi-4 14B",
|
|
1023
|
+
"size": "9.1 GB",
|
|
1024
|
+
"ram": "16 GB RAM / 10 GB VRAM",
|
|
1025
|
+
"context": "16K",
|
|
1026
|
+
"speed": "~40 tok/s (GPU)",
|
|
1027
|
+
"pros": [
|
|
1028
|
+
"🔬 State-of-the-art synthetic data training by Microsoft Research",
|
|
1029
|
+
"📐 Dense algorithmic reasoning per parameter count",
|
|
1030
|
+
],
|
|
1031
|
+
"cons": [
|
|
1032
|
+
"⚠️ Smaller 16k context window compared to Qwen 2.5",
|
|
1033
|
+
"⚠️ Strict system prompt sensitivity",
|
|
1034
|
+
],
|
|
1035
|
+
"hf_repo": "microsoft/phi-4-gguf",
|
|
1036
|
+
"ollama_tag": "phi4:14b",
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
"id": "starcoder2:15b",
|
|
1040
|
+
"name": "BigCode StarCoder2 15B",
|
|
1041
|
+
"size": "9.5 GB",
|
|
1042
|
+
"ram": "16 GB RAM / 10 GB VRAM",
|
|
1043
|
+
"context": "16K",
|
|
1044
|
+
"speed": "~40 tok/s (GPU)",
|
|
1045
|
+
"pros": [
|
|
1046
|
+
"📜 Fully open, permissively licensed training data (The Stack v2)",
|
|
1047
|
+
"🌐 600+ programming language coverage",
|
|
1048
|
+
],
|
|
1049
|
+
"cons": [
|
|
1050
|
+
"⚠️ Weaker natural language chat instruction following",
|
|
1051
|
+
],
|
|
1052
|
+
"hf_repo": "bigcode/starcoder2-15b-gguf",
|
|
1053
|
+
"ollama_tag": "starcoder2:15b",
|
|
1054
|
+
},
|
|
1055
|
+
]
|
|
1056
|
+
|
|
1057
|
+
BANKAI_CUSTOM_MODELS: List[Dict[str, Any]] = [
|
|
1058
|
+
{
|
|
1059
|
+
"id": "bankai-7b",
|
|
1060
|
+
"name": "Bankai-7B GGUF (Flagship Code & AST Healer)",
|
|
1061
|
+
"repo_id": "krishivjoshi/bankai-7b",
|
|
1062
|
+
"default_filename": "bankai-7b.gguf",
|
|
1063
|
+
"size": "4.7 GB",
|
|
1064
|
+
"ram": "1.0-4.0 GB RAM Budget",
|
|
1065
|
+
"ollama_tag": "bankai:7b",
|
|
1066
|
+
"description": "Custom fine-tuned compiler-grounded model with AST patch synthesis, test generation, and <think> chain-of-thought.",
|
|
1067
|
+
},
|
|
1068
|
+
{
|
|
1069
|
+
"id": "bankai-10b",
|
|
1070
|
+
"name": "Bankai-10B GGUF (Multi-File Architecture)",
|
|
1071
|
+
"repo_id": "krishivjoshi/bankai-10b",
|
|
1072
|
+
"default_filename": "bankai-10b.gguf",
|
|
1073
|
+
"size": "6.5 GB",
|
|
1074
|
+
"ram": "6.0 GB RAM Budget",
|
|
1075
|
+
"ollama_tag": "bankai:10b",
|
|
1076
|
+
"description": "High-capacity architecture model for 10+ file scaffolding, 3-way merge conflict resolution, and PR reviews.",
|
|
1077
|
+
},
|
|
1078
|
+
{
|
|
1079
|
+
"id": "bankai-3b",
|
|
1080
|
+
"name": "Bankai-3B GGUF (Sovereign 1.0GB RAM Edition)",
|
|
1081
|
+
"repo_id": "krishivjoshi/bankai-3b",
|
|
1082
|
+
"default_filename": "bankai-3b.gguf",
|
|
1083
|
+
"size": "2.2 GB",
|
|
1084
|
+
"ram": "1.0 GB RAM Budget",
|
|
1085
|
+
"ollama_tag": "bankai:3b",
|
|
1086
|
+
"description": "Engineered strictly for memory-constrained environments, edge devices, and 100% offline air-gapped development.",
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
"id": "bankai-1.5b",
|
|
1090
|
+
"name": "Bankai-1.5B GGUF (Fast Background Daemon)",
|
|
1091
|
+
"repo_id": "krishivjoshi/bankai-1.5b",
|
|
1092
|
+
"default_filename": "bankai-1.5b.gguf",
|
|
1093
|
+
"size": "1.1 GB",
|
|
1094
|
+
"ram": "512 MB - 1.0 GB RAM",
|
|
1095
|
+
"ollama_tag": "bankai:1.5b",
|
|
1096
|
+
"description": "Ultra-lightweight background model for Ghost crash interception, conventional commit generation, and real-time linting.",
|
|
1097
|
+
},
|
|
1098
|
+
]
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def list_local_coding_models() -> List[Dict[str, Any]]:
|
|
1102
|
+
"""Returns the list of curated local models with pros/cons."""
|
|
1103
|
+
return list(LOCAL_CODING_MODELS)
|
|
1104
|
+
|
|
1105
|
+
|
|
1106
|
+
def list_bankai_models() -> List[Dict[str, Any]]:
|
|
1107
|
+
"""Returns the list of custom Bankai models on Hugging Face."""
|
|
1108
|
+
return list(BANKAI_CUSTOM_MODELS)
|
|
1109
|
+
|