nc1709 1.15.4__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.
- nc1709/__init__.py +13 -0
- nc1709/agent/__init__.py +36 -0
- nc1709/agent/core.py +505 -0
- nc1709/agent/mcp_bridge.py +245 -0
- nc1709/agent/permissions.py +298 -0
- nc1709/agent/tools/__init__.py +21 -0
- nc1709/agent/tools/base.py +440 -0
- nc1709/agent/tools/bash_tool.py +367 -0
- nc1709/agent/tools/file_tools.py +454 -0
- nc1709/agent/tools/notebook_tools.py +516 -0
- nc1709/agent/tools/search_tools.py +322 -0
- nc1709/agent/tools/task_tool.py +284 -0
- nc1709/agent/tools/web_tools.py +555 -0
- nc1709/agents/__init__.py +17 -0
- nc1709/agents/auto_fix.py +506 -0
- nc1709/agents/test_generator.py +507 -0
- nc1709/checkpoints.py +372 -0
- nc1709/cli.py +3380 -0
- nc1709/cli_ui.py +1080 -0
- nc1709/cognitive/__init__.py +149 -0
- nc1709/cognitive/anticipation.py +594 -0
- nc1709/cognitive/context_engine.py +1046 -0
- nc1709/cognitive/council.py +824 -0
- nc1709/cognitive/learning.py +761 -0
- nc1709/cognitive/router.py +583 -0
- nc1709/cognitive/system.py +519 -0
- nc1709/config.py +155 -0
- nc1709/custom_commands.py +300 -0
- nc1709/executor.py +333 -0
- nc1709/file_controller.py +354 -0
- nc1709/git_integration.py +308 -0
- nc1709/github_integration.py +477 -0
- nc1709/image_input.py +446 -0
- nc1709/linting.py +519 -0
- nc1709/llm_adapter.py +667 -0
- nc1709/logger.py +192 -0
- nc1709/mcp/__init__.py +18 -0
- nc1709/mcp/client.py +370 -0
- nc1709/mcp/manager.py +407 -0
- nc1709/mcp/protocol.py +210 -0
- nc1709/mcp/server.py +473 -0
- nc1709/memory/__init__.py +20 -0
- nc1709/memory/embeddings.py +325 -0
- nc1709/memory/indexer.py +474 -0
- nc1709/memory/sessions.py +432 -0
- nc1709/memory/vector_store.py +451 -0
- nc1709/models/__init__.py +86 -0
- nc1709/models/detector.py +377 -0
- nc1709/models/formats.py +315 -0
- nc1709/models/manager.py +438 -0
- nc1709/models/registry.py +497 -0
- nc1709/performance/__init__.py +343 -0
- nc1709/performance/cache.py +705 -0
- nc1709/performance/pipeline.py +611 -0
- nc1709/performance/tiering.py +543 -0
- nc1709/plan_mode.py +362 -0
- nc1709/plugins/__init__.py +17 -0
- nc1709/plugins/agents/__init__.py +18 -0
- nc1709/plugins/agents/django_agent.py +912 -0
- nc1709/plugins/agents/docker_agent.py +623 -0
- nc1709/plugins/agents/fastapi_agent.py +887 -0
- nc1709/plugins/agents/git_agent.py +731 -0
- nc1709/plugins/agents/nextjs_agent.py +867 -0
- nc1709/plugins/base.py +359 -0
- nc1709/plugins/manager.py +411 -0
- nc1709/plugins/registry.py +337 -0
- nc1709/progress.py +443 -0
- nc1709/prompts/__init__.py +22 -0
- nc1709/prompts/agent_system.py +180 -0
- nc1709/prompts/task_prompts.py +340 -0
- nc1709/prompts/unified_prompt.py +133 -0
- nc1709/reasoning_engine.py +541 -0
- nc1709/remote_client.py +266 -0
- nc1709/shell_completions.py +349 -0
- nc1709/slash_commands.py +649 -0
- nc1709/task_classifier.py +408 -0
- nc1709/version_check.py +177 -0
- nc1709/web/__init__.py +8 -0
- nc1709/web/server.py +950 -0
- nc1709/web/templates/index.html +1127 -0
- nc1709-1.15.4.dist-info/METADATA +858 -0
- nc1709-1.15.4.dist-info/RECORD +86 -0
- nc1709-1.15.4.dist-info/WHEEL +5 -0
- nc1709-1.15.4.dist-info/entry_points.txt +2 -0
- nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
- nc1709-1.15.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-detect model capabilities from Ollama.
|
|
3
|
+
|
|
4
|
+
When you download a new model that's not in the registry,
|
|
5
|
+
this module can detect its basic capabilities and create a spec.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import re
|
|
10
|
+
from typing import Optional, List, Dict, Any
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import aiohttp
|
|
14
|
+
HAS_AIOHTTP = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
HAS_AIOHTTP = False
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import httpx
|
|
20
|
+
HAS_HTTPX = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
HAS_HTTPX = False
|
|
23
|
+
|
|
24
|
+
from .registry import (
|
|
25
|
+
ModelSpec, ModelCapability, PromptFormat,
|
|
26
|
+
KNOWN_MODELS, register_model
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ModelDetector:
|
|
31
|
+
"""
|
|
32
|
+
Auto-detects model capabilities from Ollama.
|
|
33
|
+
|
|
34
|
+
When you download a new model that's not in the registry,
|
|
35
|
+
this class can detect its basic capabilities.
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
detector = ModelDetector()
|
|
39
|
+
models = await detector.list_installed_models()
|
|
40
|
+
spec = await detector.detect_model_spec("new-model:latest")
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, ollama_url: str = "http://localhost:11434"):
|
|
44
|
+
self.ollama_url = ollama_url.rstrip("/")
|
|
45
|
+
|
|
46
|
+
async def list_installed_models(self) -> List[str]:
|
|
47
|
+
"""
|
|
48
|
+
Get list of models installed in Ollama.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List of model names
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
data = await self._get(f"{self.ollama_url}/api/tags")
|
|
55
|
+
if data:
|
|
56
|
+
return [model["name"] for model in data.get("models", [])]
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
def list_installed_models_sync(self) -> List[str]:
|
|
62
|
+
"""Synchronous version of list_installed_models"""
|
|
63
|
+
try:
|
|
64
|
+
return asyncio.get_event_loop().run_until_complete(
|
|
65
|
+
self.list_installed_models()
|
|
66
|
+
)
|
|
67
|
+
except RuntimeError:
|
|
68
|
+
# No event loop, create one
|
|
69
|
+
return asyncio.run(self.list_installed_models())
|
|
70
|
+
|
|
71
|
+
async def get_model_info(self, model_name: str) -> Optional[Dict[str, Any]]:
|
|
72
|
+
"""
|
|
73
|
+
Get model info from Ollama.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
model_name: The model name
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Model info dict or None
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
return await self._post(
|
|
83
|
+
f"{self.ollama_url}/api/show",
|
|
84
|
+
{"name": model_name}
|
|
85
|
+
)
|
|
86
|
+
except Exception:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
async def detect_model_spec(self, model_name: str) -> ModelSpec:
|
|
90
|
+
"""
|
|
91
|
+
Detect/create a ModelSpec for a model.
|
|
92
|
+
|
|
93
|
+
First checks the registry, then tries to auto-detect.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
model_name: The model name to detect
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
ModelSpec (from registry or auto-detected)
|
|
100
|
+
"""
|
|
101
|
+
# Check registry first
|
|
102
|
+
if model_name in KNOWN_MODELS:
|
|
103
|
+
return KNOWN_MODELS[model_name]
|
|
104
|
+
|
|
105
|
+
# Try to get info from Ollama
|
|
106
|
+
info = await self.get_model_info(model_name)
|
|
107
|
+
|
|
108
|
+
# Detect capabilities based on model name and info
|
|
109
|
+
capabilities = self._detect_capabilities(model_name, info)
|
|
110
|
+
prompt_format = self._detect_prompt_format(model_name, info)
|
|
111
|
+
context_window = self._detect_context_window(info)
|
|
112
|
+
|
|
113
|
+
# Create spec
|
|
114
|
+
spec = ModelSpec(
|
|
115
|
+
name=self._format_name(model_name),
|
|
116
|
+
ollama_name=model_name,
|
|
117
|
+
context_window=context_window,
|
|
118
|
+
prompt_format=prompt_format,
|
|
119
|
+
capabilities=capabilities,
|
|
120
|
+
suitability=self._estimate_suitability(model_name, capabilities),
|
|
121
|
+
notes="Auto-detected model"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Register it for future use
|
|
125
|
+
register_model(spec)
|
|
126
|
+
|
|
127
|
+
return spec
|
|
128
|
+
|
|
129
|
+
def detect_model_spec_sync(self, model_name: str) -> ModelSpec:
|
|
130
|
+
"""Synchronous version of detect_model_spec"""
|
|
131
|
+
try:
|
|
132
|
+
return asyncio.get_event_loop().run_until_complete(
|
|
133
|
+
self.detect_model_spec(model_name)
|
|
134
|
+
)
|
|
135
|
+
except RuntimeError:
|
|
136
|
+
return asyncio.run(self.detect_model_spec(model_name))
|
|
137
|
+
|
|
138
|
+
async def sync_with_ollama(self) -> List[ModelSpec]:
|
|
139
|
+
"""
|
|
140
|
+
Sync registry with installed Ollama models.
|
|
141
|
+
|
|
142
|
+
Detects any new models and adds them to the registry.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
List of newly detected models
|
|
146
|
+
"""
|
|
147
|
+
installed = await self.list_installed_models()
|
|
148
|
+
new_models = []
|
|
149
|
+
|
|
150
|
+
for model_name in installed:
|
|
151
|
+
if model_name not in KNOWN_MODELS:
|
|
152
|
+
spec = await self.detect_model_spec(model_name)
|
|
153
|
+
new_models.append(spec)
|
|
154
|
+
|
|
155
|
+
return new_models
|
|
156
|
+
|
|
157
|
+
def sync_with_ollama_sync(self) -> List[ModelSpec]:
|
|
158
|
+
"""Synchronous version of sync_with_ollama"""
|
|
159
|
+
try:
|
|
160
|
+
return asyncio.get_event_loop().run_until_complete(
|
|
161
|
+
self.sync_with_ollama()
|
|
162
|
+
)
|
|
163
|
+
except RuntimeError:
|
|
164
|
+
return asyncio.run(self.sync_with_ollama())
|
|
165
|
+
|
|
166
|
+
def _detect_capabilities(
|
|
167
|
+
self,
|
|
168
|
+
model_name: str,
|
|
169
|
+
info: Optional[Dict]
|
|
170
|
+
) -> List[ModelCapability]:
|
|
171
|
+
"""Detect capabilities from model name and info"""
|
|
172
|
+
capabilities = []
|
|
173
|
+
name_lower = model_name.lower()
|
|
174
|
+
|
|
175
|
+
# Detect from name
|
|
176
|
+
if "coder" in name_lower or "code" in name_lower:
|
|
177
|
+
capabilities.extend([
|
|
178
|
+
ModelCapability.CODE_GENERATION,
|
|
179
|
+
ModelCapability.CODE_COMPLETION,
|
|
180
|
+
])
|
|
181
|
+
|
|
182
|
+
if any(x in name_lower for x in ["r1", "reason", "think"]):
|
|
183
|
+
capabilities.append(ModelCapability.REASONING)
|
|
184
|
+
|
|
185
|
+
if "math" in name_lower:
|
|
186
|
+
capabilities.append(ModelCapability.MATH)
|
|
187
|
+
|
|
188
|
+
if any(x in name_lower for x in [":1b", ":3b", ":0.5b", "3b", "1b"]):
|
|
189
|
+
capabilities.append(ModelCapability.FAST_INFERENCE)
|
|
190
|
+
|
|
191
|
+
if "vision" in name_lower or "llava" in name_lower:
|
|
192
|
+
capabilities.append(ModelCapability.VISION)
|
|
193
|
+
|
|
194
|
+
if "embed" in name_lower:
|
|
195
|
+
capabilities.append(ModelCapability.EMBEDDING)
|
|
196
|
+
|
|
197
|
+
if "tool" in name_lower or "function" in name_lower:
|
|
198
|
+
capabilities.append(ModelCapability.TOOL_USE)
|
|
199
|
+
|
|
200
|
+
# Check context window from info
|
|
201
|
+
if info:
|
|
202
|
+
ctx = self._detect_context_window(info)
|
|
203
|
+
if ctx >= 100000:
|
|
204
|
+
capabilities.append(ModelCapability.LONG_CONTEXT)
|
|
205
|
+
|
|
206
|
+
# Default: at least general reasoning
|
|
207
|
+
if not capabilities:
|
|
208
|
+
capabilities.append(ModelCapability.REASONING)
|
|
209
|
+
|
|
210
|
+
return list(set(capabilities)) # Remove duplicates
|
|
211
|
+
|
|
212
|
+
def _detect_prompt_format(
|
|
213
|
+
self,
|
|
214
|
+
model_name: str,
|
|
215
|
+
info: Optional[Dict]
|
|
216
|
+
) -> PromptFormat:
|
|
217
|
+
"""Detect prompt format from model name and info"""
|
|
218
|
+
name_lower = model_name.lower()
|
|
219
|
+
|
|
220
|
+
# Check model family
|
|
221
|
+
if "qwen" in name_lower:
|
|
222
|
+
return PromptFormat.CHATML
|
|
223
|
+
if "deepseek" in name_lower:
|
|
224
|
+
return PromptFormat.DEEPSEEK
|
|
225
|
+
if "llama" in name_lower or "codellama" in name_lower:
|
|
226
|
+
return PromptFormat.LLAMA
|
|
227
|
+
if "mistral" in name_lower or "mixtral" in name_lower:
|
|
228
|
+
return PromptFormat.MISTRAL
|
|
229
|
+
if "command" in name_lower:
|
|
230
|
+
return PromptFormat.COMMAND_R
|
|
231
|
+
if "alpaca" in name_lower or "vicuna" in name_lower:
|
|
232
|
+
return PromptFormat.ALPACA
|
|
233
|
+
|
|
234
|
+
# Check template in info
|
|
235
|
+
if info and "template" in info:
|
|
236
|
+
template = info["template"].lower()
|
|
237
|
+
if "im_start" in template or "chatml" in template:
|
|
238
|
+
return PromptFormat.CHATML
|
|
239
|
+
if "[inst]" in template:
|
|
240
|
+
return PromptFormat.LLAMA
|
|
241
|
+
if "### instruction" in template:
|
|
242
|
+
return PromptFormat.ALPACA
|
|
243
|
+
|
|
244
|
+
# Default to ChatML (most common)
|
|
245
|
+
return PromptFormat.CHATML
|
|
246
|
+
|
|
247
|
+
def _detect_context_window(self, info: Optional[Dict]) -> int:
|
|
248
|
+
"""Detect context window from info"""
|
|
249
|
+
if info:
|
|
250
|
+
# Check parameters
|
|
251
|
+
params = info.get("parameters", {})
|
|
252
|
+
if isinstance(params, dict) and "num_ctx" in params:
|
|
253
|
+
return params["num_ctx"]
|
|
254
|
+
|
|
255
|
+
# Check modelfile
|
|
256
|
+
modelfile = info.get("modelfile", "")
|
|
257
|
+
if isinstance(modelfile, str) and "num_ctx" in modelfile:
|
|
258
|
+
match = re.search(r"num_ctx\s+(\d+)", modelfile)
|
|
259
|
+
if match:
|
|
260
|
+
return int(match.group(1))
|
|
261
|
+
|
|
262
|
+
# Check details
|
|
263
|
+
details = info.get("details", {})
|
|
264
|
+
if isinstance(details, dict):
|
|
265
|
+
# Some models report context in details
|
|
266
|
+
families = details.get("families", [])
|
|
267
|
+
if "llama" in families:
|
|
268
|
+
return 128000 # Llama 3 default
|
|
269
|
+
if "qwen2" in families:
|
|
270
|
+
return 32768
|
|
271
|
+
|
|
272
|
+
return 32768 # Default
|
|
273
|
+
|
|
274
|
+
def _estimate_suitability(
|
|
275
|
+
self,
|
|
276
|
+
model_name: str,
|
|
277
|
+
capabilities: List[ModelCapability]
|
|
278
|
+
) -> Dict[str, float]:
|
|
279
|
+
"""Estimate suitability scores based on name and capabilities"""
|
|
280
|
+
scores = {
|
|
281
|
+
"coding": 0.5,
|
|
282
|
+
"reasoning": 0.5,
|
|
283
|
+
"general": 0.6,
|
|
284
|
+
"fast": 0.5,
|
|
285
|
+
"instant": 0.3,
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
name_lower = model_name.lower()
|
|
289
|
+
|
|
290
|
+
# Adjust based on capabilities
|
|
291
|
+
if ModelCapability.CODE_GENERATION in capabilities:
|
|
292
|
+
scores["coding"] = 0.85
|
|
293
|
+
|
|
294
|
+
if ModelCapability.REASONING in capabilities:
|
|
295
|
+
scores["reasoning"] = 0.80
|
|
296
|
+
|
|
297
|
+
if ModelCapability.FAST_INFERENCE in capabilities:
|
|
298
|
+
scores["fast"] = 0.90
|
|
299
|
+
scores["instant"] = 0.80
|
|
300
|
+
scores["coding"] *= 0.8 # Fast models usually less capable
|
|
301
|
+
|
|
302
|
+
if ModelCapability.EMBEDDING in capabilities:
|
|
303
|
+
scores = {"embedding": 1.0}
|
|
304
|
+
return scores
|
|
305
|
+
|
|
306
|
+
# Adjust based on size hints in name
|
|
307
|
+
if any(x in name_lower for x in ["70b", "72b", "65b"]):
|
|
308
|
+
scores["coding"] = min(scores["coding"] * 1.15, 0.95)
|
|
309
|
+
scores["reasoning"] = min(scores["reasoning"] * 1.15, 0.95)
|
|
310
|
+
scores["fast"] *= 0.3
|
|
311
|
+
scores["instant"] *= 0.2
|
|
312
|
+
elif any(x in name_lower for x in ["32b", "34b", "33b"]):
|
|
313
|
+
scores["coding"] = min(scores["coding"] * 1.1, 0.92)
|
|
314
|
+
scores["reasoning"] = min(scores["reasoning"] * 1.1, 0.90)
|
|
315
|
+
scores["fast"] *= 0.5
|
|
316
|
+
elif any(x in name_lower for x in ["7b", "8b"]):
|
|
317
|
+
scores["fast"] = min(scores["fast"] * 1.2, 0.92)
|
|
318
|
+
elif any(x in name_lower for x in ["3b", "1b", "0.5b"]):
|
|
319
|
+
scores["fast"] = 0.95
|
|
320
|
+
scores["instant"] = 0.95
|
|
321
|
+
scores["coding"] *= 0.6
|
|
322
|
+
|
|
323
|
+
return scores
|
|
324
|
+
|
|
325
|
+
def _format_name(self, model_name: str) -> str:
|
|
326
|
+
"""Format model name for display"""
|
|
327
|
+
# Remove :latest suffix
|
|
328
|
+
name = model_name.replace(":latest", "")
|
|
329
|
+
# Capitalize parts
|
|
330
|
+
parts = name.replace("-", " ").replace(":", " ").split()
|
|
331
|
+
return " ".join(p.capitalize() for p in parts)
|
|
332
|
+
|
|
333
|
+
async def _get(self, url: str) -> Optional[Dict]:
|
|
334
|
+
"""HTTP GET request"""
|
|
335
|
+
if HAS_AIOHTTP:
|
|
336
|
+
async with aiohttp.ClientSession() as session:
|
|
337
|
+
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
|
|
338
|
+
if response.status == 200:
|
|
339
|
+
return await response.json()
|
|
340
|
+
elif HAS_HTTPX:
|
|
341
|
+
async with httpx.AsyncClient() as client:
|
|
342
|
+
response = await client.get(url, timeout=10)
|
|
343
|
+
if response.status_code == 200:
|
|
344
|
+
return response.json()
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
async def _post(self, url: str, data: Dict) -> Optional[Dict]:
|
|
348
|
+
"""HTTP POST request"""
|
|
349
|
+
if HAS_AIOHTTP:
|
|
350
|
+
async with aiohttp.ClientSession() as session:
|
|
351
|
+
async with session.post(
|
|
352
|
+
url, json=data, timeout=aiohttp.ClientTimeout(total=10)
|
|
353
|
+
) as response:
|
|
354
|
+
if response.status == 200:
|
|
355
|
+
return await response.json()
|
|
356
|
+
elif HAS_HTTPX:
|
|
357
|
+
async with httpx.AsyncClient() as client:
|
|
358
|
+
response = await client.post(url, json=data, timeout=10)
|
|
359
|
+
if response.status_code == 200:
|
|
360
|
+
return response.json()
|
|
361
|
+
return None
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# Convenience function
|
|
365
|
+
def auto_detect_model(model_name: str, ollama_url: str = "http://localhost:11434") -> ModelSpec:
|
|
366
|
+
"""
|
|
367
|
+
Auto-detect a model's capabilities.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
model_name: The model name
|
|
371
|
+
ollama_url: Ollama API URL
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
ModelSpec for the model
|
|
375
|
+
"""
|
|
376
|
+
detector = ModelDetector(ollama_url)
|
|
377
|
+
return detector.detect_model_spec_sync(model_name)
|
nc1709/models/formats.py
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prompt format templates for different model families.
|
|
3
|
+
|
|
4
|
+
Each model family expects prompts in a specific format. This module
|
|
5
|
+
handles the conversion automatically based on the model's prompt_format.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import List, Dict, Optional
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from .registry import PromptFormat
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Message:
|
|
15
|
+
"""A chat message"""
|
|
16
|
+
role: str # "system", "user", "assistant"
|
|
17
|
+
content: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PromptFormatter:
|
|
21
|
+
"""
|
|
22
|
+
Formats prompts for different model families.
|
|
23
|
+
|
|
24
|
+
Each model family has its own expected format. This class
|
|
25
|
+
handles the conversion automatically.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
formatter = PromptFormatter()
|
|
29
|
+
messages = [
|
|
30
|
+
Message("system", "You are helpful"),
|
|
31
|
+
Message("user", "Hello!"),
|
|
32
|
+
]
|
|
33
|
+
prompt = formatter.format(messages, PromptFormat.CHATML)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def format(
|
|
38
|
+
messages: List[Message],
|
|
39
|
+
prompt_format: PromptFormat,
|
|
40
|
+
add_generation_prompt: bool = True
|
|
41
|
+
) -> str:
|
|
42
|
+
"""
|
|
43
|
+
Format messages for a specific model format.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
messages: List of messages
|
|
47
|
+
prompt_format: The format to use
|
|
48
|
+
add_generation_prompt: Whether to add the assistant prompt at end
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Formatted prompt string
|
|
52
|
+
"""
|
|
53
|
+
formatters = {
|
|
54
|
+
PromptFormat.CHATML: PromptFormatter._format_chatml,
|
|
55
|
+
PromptFormat.LLAMA: PromptFormatter._format_llama,
|
|
56
|
+
PromptFormat.ALPACA: PromptFormatter._format_alpaca,
|
|
57
|
+
PromptFormat.RAW: PromptFormatter._format_raw,
|
|
58
|
+
PromptFormat.DEEPSEEK: PromptFormatter._format_deepseek,
|
|
59
|
+
PromptFormat.MISTRAL: PromptFormatter._format_mistral,
|
|
60
|
+
PromptFormat.COMMAND_R: PromptFormatter._format_command_r,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
formatter = formatters.get(prompt_format, PromptFormatter._format_raw)
|
|
64
|
+
return formatter(messages, add_generation_prompt)
|
|
65
|
+
|
|
66
|
+
@staticmethod
|
|
67
|
+
def format_from_dicts(
|
|
68
|
+
messages: List[Dict[str, str]],
|
|
69
|
+
prompt_format: PromptFormat,
|
|
70
|
+
add_generation_prompt: bool = True
|
|
71
|
+
) -> str:
|
|
72
|
+
"""
|
|
73
|
+
Format messages from dict format.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
messages: List of {"role": "...", "content": "..."} dicts
|
|
77
|
+
prompt_format: The format to use
|
|
78
|
+
add_generation_prompt: Whether to add the assistant prompt at end
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Formatted prompt string
|
|
82
|
+
"""
|
|
83
|
+
msg_objects = [
|
|
84
|
+
Message(role=m["role"], content=m["content"])
|
|
85
|
+
for m in messages
|
|
86
|
+
]
|
|
87
|
+
return PromptFormatter.format(msg_objects, prompt_format, add_generation_prompt)
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _format_chatml(messages: List[Message], add_gen: bool) -> str:
|
|
91
|
+
"""
|
|
92
|
+
ChatML format (Qwen, OpenAI-style)
|
|
93
|
+
|
|
94
|
+
<|im_start|>system
|
|
95
|
+
You are helpful<|im_end|>
|
|
96
|
+
<|im_start|>user
|
|
97
|
+
Hello!<|im_end|>
|
|
98
|
+
<|im_start|>assistant
|
|
99
|
+
"""
|
|
100
|
+
parts = []
|
|
101
|
+
for msg in messages:
|
|
102
|
+
parts.append(f"<|im_start|>{msg.role}\n{msg.content}<|im_end|>")
|
|
103
|
+
|
|
104
|
+
if add_gen:
|
|
105
|
+
parts.append("<|im_start|>assistant\n")
|
|
106
|
+
|
|
107
|
+
return "\n".join(parts)
|
|
108
|
+
|
|
109
|
+
@staticmethod
|
|
110
|
+
def _format_llama(messages: List[Message], add_gen: bool) -> str:
|
|
111
|
+
"""
|
|
112
|
+
Llama/Llama2/Llama3 format
|
|
113
|
+
|
|
114
|
+
[INST] <<SYS>>
|
|
115
|
+
System prompt
|
|
116
|
+
<</SYS>>
|
|
117
|
+
|
|
118
|
+
User message [/INST] Assistant response
|
|
119
|
+
"""
|
|
120
|
+
parts = []
|
|
121
|
+
system_msg = None
|
|
122
|
+
|
|
123
|
+
for msg in messages:
|
|
124
|
+
if msg.role == "system":
|
|
125
|
+
system_msg = msg.content
|
|
126
|
+
elif msg.role == "user":
|
|
127
|
+
if system_msg:
|
|
128
|
+
parts.append(
|
|
129
|
+
f"[INST] <<SYS>>\n{system_msg}\n<</SYS>>\n\n{msg.content} [/INST]"
|
|
130
|
+
)
|
|
131
|
+
system_msg = None
|
|
132
|
+
else:
|
|
133
|
+
parts.append(f"[INST] {msg.content} [/INST]")
|
|
134
|
+
elif msg.role == "assistant":
|
|
135
|
+
parts.append(msg.content)
|
|
136
|
+
|
|
137
|
+
return "\n".join(parts)
|
|
138
|
+
|
|
139
|
+
@staticmethod
|
|
140
|
+
def _format_alpaca(messages: List[Message], add_gen: bool) -> str:
|
|
141
|
+
"""
|
|
142
|
+
Alpaca format
|
|
143
|
+
|
|
144
|
+
### System:
|
|
145
|
+
System prompt
|
|
146
|
+
|
|
147
|
+
### Instruction:
|
|
148
|
+
User message
|
|
149
|
+
|
|
150
|
+
### Response:
|
|
151
|
+
"""
|
|
152
|
+
parts = []
|
|
153
|
+
|
|
154
|
+
for msg in messages:
|
|
155
|
+
if msg.role == "system":
|
|
156
|
+
parts.append(f"### System:\n{msg.content}\n")
|
|
157
|
+
elif msg.role == "user":
|
|
158
|
+
parts.append(f"### Instruction:\n{msg.content}\n")
|
|
159
|
+
elif msg.role == "assistant":
|
|
160
|
+
parts.append(f"### Response:\n{msg.content}\n")
|
|
161
|
+
|
|
162
|
+
if add_gen:
|
|
163
|
+
parts.append("### Response:\n")
|
|
164
|
+
|
|
165
|
+
return "\n".join(parts)
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def _format_deepseek(messages: List[Message], add_gen: bool) -> str:
|
|
169
|
+
"""
|
|
170
|
+
DeepSeek format
|
|
171
|
+
|
|
172
|
+
<|system|>
|
|
173
|
+
System prompt
|
|
174
|
+
<|user|>
|
|
175
|
+
User message
|
|
176
|
+
<|assistant|>
|
|
177
|
+
"""
|
|
178
|
+
parts = []
|
|
179
|
+
|
|
180
|
+
for msg in messages:
|
|
181
|
+
if msg.role == "system":
|
|
182
|
+
parts.append(f"<|system|>\n{msg.content}")
|
|
183
|
+
elif msg.role == "user":
|
|
184
|
+
parts.append(f"<|user|>\n{msg.content}")
|
|
185
|
+
elif msg.role == "assistant":
|
|
186
|
+
parts.append(f"<|assistant|>\n{msg.content}")
|
|
187
|
+
|
|
188
|
+
if add_gen:
|
|
189
|
+
parts.append("<|assistant|>\n")
|
|
190
|
+
|
|
191
|
+
return "\n".join(parts)
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def _format_mistral(messages: List[Message], add_gen: bool) -> str:
|
|
195
|
+
"""
|
|
196
|
+
Mistral format
|
|
197
|
+
|
|
198
|
+
[INST] User message [/INST] Assistant response
|
|
199
|
+
"""
|
|
200
|
+
parts = []
|
|
201
|
+
|
|
202
|
+
for msg in messages:
|
|
203
|
+
if msg.role == "user":
|
|
204
|
+
parts.append(f"[INST] {msg.content} [/INST]")
|
|
205
|
+
elif msg.role == "assistant":
|
|
206
|
+
parts.append(msg.content)
|
|
207
|
+
elif msg.role == "system":
|
|
208
|
+
# Mistral often handles system as first user message
|
|
209
|
+
parts.append(f"[INST] {msg.content}\n")
|
|
210
|
+
|
|
211
|
+
return "".join(parts)
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def _format_command_r(messages: List[Message], add_gen: bool) -> str:
|
|
215
|
+
"""
|
|
216
|
+
Cohere Command-R format
|
|
217
|
+
|
|
218
|
+
<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>...<|END_OF_TURN_TOKEN|>
|
|
219
|
+
<|START_OF_TURN_TOKEN|><|USER_TOKEN|>...<|END_OF_TURN_TOKEN|>
|
|
220
|
+
<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>
|
|
221
|
+
"""
|
|
222
|
+
parts = []
|
|
223
|
+
|
|
224
|
+
for msg in messages:
|
|
225
|
+
if msg.role == "system":
|
|
226
|
+
parts.append(
|
|
227
|
+
f"<|START_OF_TURN_TOKEN|><|SYSTEM_TOKEN|>{msg.content}<|END_OF_TURN_TOKEN|>"
|
|
228
|
+
)
|
|
229
|
+
elif msg.role == "user":
|
|
230
|
+
parts.append(
|
|
231
|
+
f"<|START_OF_TURN_TOKEN|><|USER_TOKEN|>{msg.content}<|END_OF_TURN_TOKEN|>"
|
|
232
|
+
)
|
|
233
|
+
elif msg.role == "assistant":
|
|
234
|
+
parts.append(
|
|
235
|
+
f"<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>{msg.content}<|END_OF_TURN_TOKEN|>"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if add_gen:
|
|
239
|
+
parts.append("<|START_OF_TURN_TOKEN|><|CHATBOT_TOKEN|>")
|
|
240
|
+
|
|
241
|
+
return "".join(parts)
|
|
242
|
+
|
|
243
|
+
@staticmethod
|
|
244
|
+
def _format_raw(messages: List[Message], add_gen: bool) -> str:
|
|
245
|
+
"""
|
|
246
|
+
Raw format (no special tokens)
|
|
247
|
+
|
|
248
|
+
System: ...
|
|
249
|
+
User: ...
|
|
250
|
+
Assistant: ...
|
|
251
|
+
"""
|
|
252
|
+
parts = []
|
|
253
|
+
|
|
254
|
+
for msg in messages:
|
|
255
|
+
if msg.role == "system":
|
|
256
|
+
parts.append(f"System: {msg.content}\n")
|
|
257
|
+
elif msg.role == "user":
|
|
258
|
+
parts.append(f"User: {msg.content}\n")
|
|
259
|
+
elif msg.role == "assistant":
|
|
260
|
+
parts.append(f"Assistant: {msg.content}\n")
|
|
261
|
+
|
|
262
|
+
if add_gen:
|
|
263
|
+
parts.append("Assistant: ")
|
|
264
|
+
|
|
265
|
+
return "".join(parts)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def get_format_info(prompt_format: PromptFormat) -> Dict[str, str]:
|
|
269
|
+
"""
|
|
270
|
+
Get information about a prompt format.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
prompt_format: The format to describe
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Dict with format details
|
|
277
|
+
"""
|
|
278
|
+
info = {
|
|
279
|
+
PromptFormat.CHATML: {
|
|
280
|
+
"name": "ChatML",
|
|
281
|
+
"description": "OpenAI-style format used by Qwen, etc.",
|
|
282
|
+
"example": "<|im_start|>user\\nHello<|im_end|>",
|
|
283
|
+
},
|
|
284
|
+
PromptFormat.LLAMA: {
|
|
285
|
+
"name": "Llama",
|
|
286
|
+
"description": "Meta Llama format with [INST] tags",
|
|
287
|
+
"example": "[INST] Hello [/INST]",
|
|
288
|
+
},
|
|
289
|
+
PromptFormat.ALPACA: {
|
|
290
|
+
"name": "Alpaca",
|
|
291
|
+
"description": "Stanford Alpaca instruction format",
|
|
292
|
+
"example": "### Instruction:\\nHello\\n### Response:",
|
|
293
|
+
},
|
|
294
|
+
PromptFormat.DEEPSEEK: {
|
|
295
|
+
"name": "DeepSeek",
|
|
296
|
+
"description": "DeepSeek model format",
|
|
297
|
+
"example": "<|user|>\\nHello\\n<|assistant|>",
|
|
298
|
+
},
|
|
299
|
+
PromptFormat.MISTRAL: {
|
|
300
|
+
"name": "Mistral",
|
|
301
|
+
"description": "Mistral AI format",
|
|
302
|
+
"example": "[INST] Hello [/INST]",
|
|
303
|
+
},
|
|
304
|
+
PromptFormat.COMMAND_R: {
|
|
305
|
+
"name": "Command-R",
|
|
306
|
+
"description": "Cohere Command-R format",
|
|
307
|
+
"example": "<|START_OF_TURN_TOKEN|><|USER_TOKEN|>Hello",
|
|
308
|
+
},
|
|
309
|
+
PromptFormat.RAW: {
|
|
310
|
+
"name": "Raw",
|
|
311
|
+
"description": "Plain text with role prefixes",
|
|
312
|
+
"example": "User: Hello\\nAssistant:",
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
return info.get(prompt_format, {"name": "Unknown", "description": "", "example": ""})
|