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.
Files changed (86) hide show
  1. nc1709/__init__.py +13 -0
  2. nc1709/agent/__init__.py +36 -0
  3. nc1709/agent/core.py +505 -0
  4. nc1709/agent/mcp_bridge.py +245 -0
  5. nc1709/agent/permissions.py +298 -0
  6. nc1709/agent/tools/__init__.py +21 -0
  7. nc1709/agent/tools/base.py +440 -0
  8. nc1709/agent/tools/bash_tool.py +367 -0
  9. nc1709/agent/tools/file_tools.py +454 -0
  10. nc1709/agent/tools/notebook_tools.py +516 -0
  11. nc1709/agent/tools/search_tools.py +322 -0
  12. nc1709/agent/tools/task_tool.py +284 -0
  13. nc1709/agent/tools/web_tools.py +555 -0
  14. nc1709/agents/__init__.py +17 -0
  15. nc1709/agents/auto_fix.py +506 -0
  16. nc1709/agents/test_generator.py +507 -0
  17. nc1709/checkpoints.py +372 -0
  18. nc1709/cli.py +3380 -0
  19. nc1709/cli_ui.py +1080 -0
  20. nc1709/cognitive/__init__.py +149 -0
  21. nc1709/cognitive/anticipation.py +594 -0
  22. nc1709/cognitive/context_engine.py +1046 -0
  23. nc1709/cognitive/council.py +824 -0
  24. nc1709/cognitive/learning.py +761 -0
  25. nc1709/cognitive/router.py +583 -0
  26. nc1709/cognitive/system.py +519 -0
  27. nc1709/config.py +155 -0
  28. nc1709/custom_commands.py +300 -0
  29. nc1709/executor.py +333 -0
  30. nc1709/file_controller.py +354 -0
  31. nc1709/git_integration.py +308 -0
  32. nc1709/github_integration.py +477 -0
  33. nc1709/image_input.py +446 -0
  34. nc1709/linting.py +519 -0
  35. nc1709/llm_adapter.py +667 -0
  36. nc1709/logger.py +192 -0
  37. nc1709/mcp/__init__.py +18 -0
  38. nc1709/mcp/client.py +370 -0
  39. nc1709/mcp/manager.py +407 -0
  40. nc1709/mcp/protocol.py +210 -0
  41. nc1709/mcp/server.py +473 -0
  42. nc1709/memory/__init__.py +20 -0
  43. nc1709/memory/embeddings.py +325 -0
  44. nc1709/memory/indexer.py +474 -0
  45. nc1709/memory/sessions.py +432 -0
  46. nc1709/memory/vector_store.py +451 -0
  47. nc1709/models/__init__.py +86 -0
  48. nc1709/models/detector.py +377 -0
  49. nc1709/models/formats.py +315 -0
  50. nc1709/models/manager.py +438 -0
  51. nc1709/models/registry.py +497 -0
  52. nc1709/performance/__init__.py +343 -0
  53. nc1709/performance/cache.py +705 -0
  54. nc1709/performance/pipeline.py +611 -0
  55. nc1709/performance/tiering.py +543 -0
  56. nc1709/plan_mode.py +362 -0
  57. nc1709/plugins/__init__.py +17 -0
  58. nc1709/plugins/agents/__init__.py +18 -0
  59. nc1709/plugins/agents/django_agent.py +912 -0
  60. nc1709/plugins/agents/docker_agent.py +623 -0
  61. nc1709/plugins/agents/fastapi_agent.py +887 -0
  62. nc1709/plugins/agents/git_agent.py +731 -0
  63. nc1709/plugins/agents/nextjs_agent.py +867 -0
  64. nc1709/plugins/base.py +359 -0
  65. nc1709/plugins/manager.py +411 -0
  66. nc1709/plugins/registry.py +337 -0
  67. nc1709/progress.py +443 -0
  68. nc1709/prompts/__init__.py +22 -0
  69. nc1709/prompts/agent_system.py +180 -0
  70. nc1709/prompts/task_prompts.py +340 -0
  71. nc1709/prompts/unified_prompt.py +133 -0
  72. nc1709/reasoning_engine.py +541 -0
  73. nc1709/remote_client.py +266 -0
  74. nc1709/shell_completions.py +349 -0
  75. nc1709/slash_commands.py +649 -0
  76. nc1709/task_classifier.py +408 -0
  77. nc1709/version_check.py +177 -0
  78. nc1709/web/__init__.py +8 -0
  79. nc1709/web/server.py +950 -0
  80. nc1709/web/templates/index.html +1127 -0
  81. nc1709-1.15.4.dist-info/METADATA +858 -0
  82. nc1709-1.15.4.dist-info/RECORD +86 -0
  83. nc1709-1.15.4.dist-info/WHEEL +5 -0
  84. nc1709-1.15.4.dist-info/entry_points.txt +2 -0
  85. nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
  86. 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)
@@ -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": ""})