modelforge-finetuning 3.0.3__tar.gz → 3.0.4__tar.gz

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 (109) hide show
  1. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/cli.py +2 -2
  2. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/notebook_cli/__init__.py +2 -1
  3. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/notebook_cli/progress.py +11 -0
  4. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/notebook_cli/prompts.py +57 -0
  5. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/notebook_cli/wizard.py +145 -37
  6. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/strategies/qlora_strategy.py +7 -3
  7. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/strategies/sft_strategy.py +7 -2
  8. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/strategies/strategy_factory.py +34 -14
  9. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/utilities/chat_playground.py +1 -1
  10. {modelforge_finetuning-3.0.3/modelforge_finetuning.egg-info → modelforge_finetuning-3.0.4}/PKG-INFO +4 -1
  11. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4/modelforge_finetuning.egg-info}/PKG-INFO +4 -1
  12. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/modelforge_finetuning.egg-info/requires.txt +4 -0
  13. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/pyproject.toml +5 -1
  14. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/LICENSE +0 -0
  15. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/MANIFEST.in +0 -0
  16. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/asset-manifest.json +0 -0
  17. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/favicon.ico +0 -0
  18. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/favicon.jpg +0 -0
  19. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/index.html +0 -0
  20. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/logo192.png +0 -0
  21. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/logo512.png +0 -0
  22. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/manifest.json +0 -0
  23. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/robots.txt +0 -0
  24. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/static/css/main.5a3d6ea1.css +0 -0
  25. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/static/css/main.5a3d6ea1.css.map +0 -0
  26. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/static/css/main.9680427d.css +0 -0
  27. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/static/css/main.9680427d.css.map +0 -0
  28. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/static/js/453.6e4f956a.chunk.js +0 -0
  29. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/static/js/453.6e4f956a.chunk.js.map +0 -0
  30. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/static/js/453.ad6eb26e.chunk.js +0 -0
  31. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/static/js/453.ad6eb26e.chunk.js.map +0 -0
  32. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/static/js/main.2b0344e0.js +0 -0
  33. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/static/js/main.2b0344e0.js.LICENSE.txt +0 -0
  34. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/static/js/main.2b0344e0.js.map +0 -0
  35. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/static/js/main.9cc9257a.js +0 -0
  36. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/static/js/main.9cc9257a.js.LICENSE.txt +0 -0
  37. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/static/js/main.9cc9257a.js.map +0 -0
  38. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/static/js/main.9df15fbf.js +0 -0
  39. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/static/js/main.9df15fbf.js.LICENSE.txt +0 -0
  40. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/Frontend/build/static/js/main.9df15fbf.js.map +0 -0
  41. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/__init__.py +0 -0
  42. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/app.py +0 -0
  43. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/database/__init__.py +0 -0
  44. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/database/database_manager.py +0 -0
  45. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/database/models.py +0 -0
  46. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/dependencies.py +0 -0
  47. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/evaluation/__init__.py +0 -0
  48. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/evaluation/dataset_validator.py +0 -0
  49. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/evaluation/metrics.py +0 -0
  50. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/exceptions.py +0 -0
  51. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/formatters/__init__.py +0 -0
  52. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/formatters/formatting_functions.py +0 -0
  53. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/globals/__init__.py +0 -0
  54. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/globals/globals.py +0 -0
  55. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/globals/globals_instance.py +0 -0
  56. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/logging_config.py +0 -0
  57. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/model_configs/high_end.json +0 -0
  58. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/model_configs/low_end.json +0 -0
  59. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/model_configs/mid_range.json +0 -0
  60. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/providers/__init__.py +0 -0
  61. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/providers/huggingface_provider.py +0 -0
  62. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/providers/provider_factory.py +0 -0
  63. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/providers/unsloth_provider.py +0 -0
  64. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/routers/__init__.py +0 -0
  65. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/routers/finetuning_router.py +0 -0
  66. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/routers/hub_management_router.py +0 -0
  67. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/routers/models_router.py +0 -0
  68. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/routers/playground_router.py +0 -0
  69. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/schemas/__init__.py +0 -0
  70. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/schemas/training_schemas.py +0 -0
  71. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/services/__init__.py +0 -0
  72. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/services/hardware_service.py +0 -0
  73. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/services/model_service.py +0 -0
  74. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/services/training_service.py +0 -0
  75. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/strategies/__init__.py +0 -0
  76. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/strategies/dpo_strategy.py +0 -0
  77. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/strategies/rlhf_strategy.py +0 -0
  78. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/test_datasets/low_qa_train_set.jsonl +0 -0
  79. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/test_datasets/low_summarization_train_set.jsonl +0 -0
  80. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/test_datasets/low_text_generation.jsonl +0 -0
  81. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/test_datasets/med_construction.jsonl +0 -0
  82. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/test_datasets/med_text_generation.jsonl +0 -0
  83. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/test_datasets/question_answer-train-data-sample.jsonl +0 -0
  84. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/test_datasets/test_dataset.json +0 -0
  85. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/test_datasets/test_financial_advisory_generation.jsonl +0 -0
  86. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/test_datasets/test_financial_advisory_generation_bak.jsonl +0 -0
  87. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/utilities/__init__.py +0 -0
  88. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/utilities/configs/hardware_thresholds.json +0 -0
  89. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/utilities/configs/tasks.json +0 -0
  90. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/utilities/device_utils.py +0 -0
  91. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/utilities/finetuning/__init__.py +0 -0
  92. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/utilities/finetuning/quantization.py +0 -0
  93. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/utilities/finetuning/settings_builder.py +0 -0
  94. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/utilities/hardware_detection/__init__.py +0 -0
  95. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/utilities/hardware_detection/config_manager.py +0 -0
  96. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/utilities/hardware_detection/hardware_detector.py +0 -0
  97. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/utilities/hardware_detection/hardware_detector_factory.py +0 -0
  98. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/utilities/hardware_detection/model_recommendation.py +0 -0
  99. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/utilities/hardware_detection/model_validator.py +0 -0
  100. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/utilities/settings_managers/DBManager.py +0 -0
  101. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/utilities/settings_managers/FileManager.py +0 -0
  102. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/ModelForge/utilities/settings_managers/__init__.py +0 -0
  103. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/README.md +0 -0
  104. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/modelforge_finetuning.egg-info/SOURCES.txt +0 -0
  105. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/modelforge_finetuning.egg-info/dependency_links.txt +0 -0
  106. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/modelforge_finetuning.egg-info/entry_points.txt +0 -0
  107. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/modelforge_finetuning.egg-info/top_level.txt +0 -0
  108. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/setup.cfg +0 -0
  109. {modelforge_finetuning-3.0.3 → modelforge_finetuning-3.0.4}/tests/test_device_utils.py +0 -0
@@ -65,14 +65,14 @@ def main():
65
65
  subcommand = sys.argv[1]
66
66
  if subcommand == "cli":
67
67
  try:
68
- from .notebook_cli.wizard import main as cli_main
68
+ from .notebook_cli.wizard import ModelForgeWizard
69
69
  except ImportError as e:
70
70
  logger.error(f"Failed to import CLI wizard or its dependencies: {e}")
71
71
  print("\nThe interactive CLI requires optional dependencies that are not installed.")
72
72
  print("Please install the CLI extras and try again, for example:")
73
73
  print(" pip install \"modelforge-finetuning[cli]\"")
74
74
  sys.exit(1)
75
- cli_main()
75
+ ModelForgeWizard(notebook=False).run()
76
76
  return
77
77
  else:
78
78
  print(f"Unknown subcommand: '{subcommand}'")
@@ -12,7 +12,8 @@ Usage from a notebook cell:
12
12
  run_cli()
13
13
 
14
14
  Usage from a terminal:
15
- modelforge-nb
15
+ modelforge-nb (notebook-safe input() prompts)
16
+ modelforge cli (interactive terminal with questionary)
16
17
  """
17
18
  from .wizard import ModelForgeWizard, main as run_cli
18
19
 
@@ -7,8 +7,13 @@ notebook environment, and falls back to tqdm for plain terminal use.
7
7
  from __future__ import annotations
8
8
 
9
9
 
10
+ _NOTEBOOK_OVERRIDE: bool | None = None
11
+
12
+
10
13
  def _is_notebook() -> bool:
11
14
  """Return True if running inside a Jupyter-compatible notebook kernel."""
15
+ if _NOTEBOOK_OVERRIDE is not None:
16
+ return _NOTEBOOK_OVERRIDE
12
17
  try:
13
18
  from IPython import get_ipython
14
19
  shell = get_ipython()
@@ -19,6 +24,12 @@ def _is_notebook() -> bool:
19
24
  return False
20
25
 
21
26
 
27
+ def set_notebook_mode(mode: bool) -> None:
28
+ """Override the auto-detected notebook mode."""
29
+ global _NOTEBOOK_OVERRIDE
30
+ _NOTEBOOK_OVERRIDE = mode
31
+
32
+
22
33
  class ProgressDisplay:
23
34
  """
24
35
  Unified progress display that works in notebooks and terminals.
@@ -33,6 +33,12 @@ def _is_notebook() -> bool:
33
33
  _IN_NOTEBOOK = _is_notebook()
34
34
 
35
35
 
36
+ def set_notebook_mode(mode: bool) -> None:
37
+ """Override the auto-detected notebook mode."""
38
+ global _IN_NOTEBOOK
39
+ _IN_NOTEBOOK = mode
40
+
41
+
36
42
  # ── Notebook-safe input() fallbacks ──────────────────────────────────────────
37
43
 
38
44
  def _nb_select(message: str, choices: list) -> str:
@@ -83,6 +89,14 @@ def _nb_confirm(message: str, default: bool = True) -> bool:
83
89
 
84
90
  # ── Choices ────────────────────────────────────────────────────────────────
85
91
 
92
+ _MAIN_MENU_CHOICES = [
93
+ questionary.Choice("Fine-tune a model", value="train"),
94
+ questionary.Choice("List past models", value="list"),
95
+ questionary.Choice("Test a fine-tuned model", value="test"),
96
+ questionary.Choice("Exit", value="exit"),
97
+ ]
98
+
99
+
86
100
  _TASK_CHOICES = [
87
101
  questionary.Choice("Text Generation (causal LM fine-tuning)", value="text-generation"),
88
102
  questionary.Choice("Summarization (seq-to-seq fine-tuning)", value="summarization"),
@@ -342,6 +356,49 @@ def prompt_hyperparams(defaults: dict) -> dict:
342
356
  return params
343
357
 
344
358
 
359
+ def prompt_main_menu() -> str:
360
+ """Ask the user what they want to do."""
361
+ if _IN_NOTEBOOK:
362
+ return _nb_select("What would you like to do?", _MAIN_MENU_CHOICES)
363
+ return questionary.select(
364
+ "What would you like to do?",
365
+ choices=_MAIN_MENU_CHOICES,
366
+ style=questionary.Style([("selected", "fg:cyan bold")]),
367
+ ).ask()
368
+
369
+
370
+ def prompt_select_model(models: list[dict]) -> dict | None:
371
+ """
372
+ Ask the user to select a model from a list.
373
+
374
+ Args:
375
+ models: List of model dictionaries from the database.
376
+
377
+ Returns:
378
+ Selected model dict, or None if cancelled.
379
+ """
380
+ choices = []
381
+ for m in models:
382
+ label = (
383
+ f"{m.get('name', 'unknown')} "
384
+ f"[{m.get('task', '?')} / {m.get('strategy', '?')}]"
385
+ )
386
+ choices.append(questionary.Choice(label, value=m["id"]))
387
+
388
+ if _IN_NOTEBOOK:
389
+ model_id = _nb_select("Select a model:", choices)
390
+ else:
391
+ model_id = questionary.select(
392
+ "Select a model:",
393
+ choices=choices,
394
+ style=questionary.Style([("selected", "fg:cyan bold")]),
395
+ ).ask()
396
+
397
+ if model_id is None:
398
+ return None
399
+ return next((m for m in models if m["id"] == model_id), None)
400
+
401
+
345
402
  def prompt_confirm_config(config: dict) -> bool:
346
403
  """
347
404
  Show a summary table of the final config and ask for confirmation.
@@ -30,57 +30,165 @@ class ModelForgeWizard:
30
30
  Uses existing Python service classes directly — no REST API calls.
31
31
  """
32
32
 
33
- def __init__(self):
33
+ def __init__(self, notebook: bool | None = None):
34
34
  from rich.console import Console
35
35
 
36
36
  self.console = Console()
37
37
 
38
+ if notebook is not None:
39
+ from . import prompts, progress
40
+ prompts.set_notebook_mode(notebook)
41
+ progress.set_notebook_mode(notebook)
42
+
38
43
  # ── Public ─────────────────────────────────────────────────────────────
39
44
 
40
45
  def run(self) -> None:
41
- """Execute the full wizard flow."""
42
- from pydantic import ValidationError
43
-
44
- from ..schemas.training_schemas import TrainingConfig
45
- from .prompts import prompt_confirm_config
46
+ """Show main menu in a loop, dispatching until the user exits."""
47
+ from .prompts import prompt_main_menu
46
48
 
47
49
  try:
48
50
  self._print_banner()
49
- hardware_info = self._detect_hardware()
50
- task = self._select_task()
51
- model_name = self._select_model(task, hardware_info)
52
- provider = self._select_provider()
53
- strategy = self._select_strategy()
54
- dataset_path = self._select_dataset(task, strategy)
55
- hyperparams = self._configure_hyperparams(hardware_info, strategy)
56
- config = self._build_config(
57
- task=task,
58
- model_name=model_name,
59
- provider=provider,
60
- strategy=strategy,
61
- dataset=dataset_path,
62
- compute_specs=hardware_info.get("compute_profile", "low_end"),
63
- hyperparams=hyperparams,
64
- )
65
- try:
66
- TrainingConfig(**config)
67
- except ValidationError as exc:
68
- errors = "; ".join(e["msg"] for e in exc.errors())
69
- self.console.print(
70
- f"\n[red bold]Invalid configuration:[/red bold] {errors}"
71
- )
72
- return
73
- confirmed = prompt_confirm_config(config)
74
- if not confirmed:
75
- self.console.print("\n[yellow]Training cancelled.[/yellow]")
76
- return
77
- self._run_training(config)
51
+ while True:
52
+ action = prompt_main_menu()
53
+ if action is None or action == "exit":
54
+ self.console.print("\n[dim]Goodbye![/dim]")
55
+ return
56
+ if action == "train":
57
+ self._run_train_wizard()
58
+ elif action == "list":
59
+ self._list_models()
60
+ elif action == "test":
61
+ self._test_model()
78
62
  except KeyboardInterrupt:
79
63
  self.console.print("\n\n[yellow]Interrupted — goodbye.[/yellow]")
80
64
  except Exception as exc:
81
65
  self.console.print(f"\n[red bold]Error:[/red bold] {exc}")
82
66
  raise
83
67
 
68
+ def _run_train_wizard(self) -> None:
69
+ """Execute the full training wizard flow."""
70
+ from pydantic import ValidationError
71
+
72
+ from ..schemas.training_schemas import TrainingConfig
73
+ from .prompts import prompt_confirm_config
74
+
75
+ hardware_info = self._detect_hardware()
76
+ task = self._select_task()
77
+ model_name = self._select_model(task, hardware_info)
78
+ provider = self._select_provider()
79
+ strategy = self._select_strategy()
80
+ dataset_path = self._select_dataset(task, strategy)
81
+ hyperparams = self._configure_hyperparams(hardware_info, strategy)
82
+ config = self._build_config(
83
+ task=task,
84
+ model_name=model_name,
85
+ provider=provider,
86
+ strategy=strategy,
87
+ dataset=dataset_path,
88
+ compute_specs=hardware_info.get("compute_profile", "low_end"),
89
+ hyperparams=hyperparams,
90
+ )
91
+ try:
92
+ TrainingConfig(**config)
93
+ except ValidationError as exc:
94
+ errors = "; ".join(e["msg"] for e in exc.errors())
95
+ self.console.print(
96
+ f"\n[red bold]Invalid configuration:[/red bold] {errors}"
97
+ )
98
+ return
99
+ confirmed = prompt_confirm_config(config)
100
+ if not confirmed:
101
+ self.console.print("\n[yellow]Training cancelled.[/yellow]")
102
+ return
103
+ self._run_training(config)
104
+
105
+ # ── List & Test ────────────────────────────────────────────────────────
106
+
107
+ def _list_models(self) -> None:
108
+ """Display a table of all fine-tuned models."""
109
+ from rich.table import Table
110
+
111
+ from ..dependencies import get_model_service
112
+
113
+ self.console.print("\n[bold]Past Fine-Tuned Models[/bold]\n")
114
+ model_service = get_model_service()
115
+ models = model_service.get_all_models()
116
+
117
+ if not models:
118
+ self.console.print(" [dim]No models found.[/dim]")
119
+ return
120
+
121
+ table = Table(show_header=True, header_style="bold cyan")
122
+ table.add_column("#", style="dim", width=3)
123
+ table.add_column("Name")
124
+ table.add_column("Base Model")
125
+ table.add_column("Task")
126
+ table.add_column("Strategy")
127
+ table.add_column("Provider")
128
+ table.add_column("Created")
129
+ table.add_column("Path", style="dim")
130
+
131
+ for i, m in enumerate(models, 1):
132
+ created = m.get("created_at", "")
133
+ if created:
134
+ created = created[:16].replace("T", " ")
135
+ table.add_row(
136
+ str(i),
137
+ m.get("name", "—"),
138
+ m.get("base_model", "—"),
139
+ m.get("task", "—"),
140
+ m.get("strategy", "—"),
141
+ m.get("provider", "—"),
142
+ created,
143
+ m.get("path", "—"),
144
+ )
145
+
146
+ self.console.print(table)
147
+
148
+ def _test_model(self) -> None:
149
+ """Let the user pick a fine-tuned model and launch the playground."""
150
+ import os
151
+
152
+ from rich.panel import Panel
153
+
154
+ from ..dependencies import get_model_service
155
+ from .prompts import prompt_select_model
156
+
157
+ self.console.print("\n[bold]Test a Fine-Tuned Model[/bold]\n")
158
+ model_service = get_model_service()
159
+ models = model_service.get_all_models()
160
+
161
+ if not models:
162
+ self.console.print(" [dim]No models found. Fine-tune a model first.[/dim]")
163
+ return
164
+
165
+ selected = prompt_select_model(models)
166
+ if selected is None:
167
+ return
168
+
169
+ model_path = selected.get("path", "")
170
+ if not model_path or not os.path.isdir(model_path):
171
+ self.console.print(
172
+ f"\n[red]Model path not found:[/red] {model_path}"
173
+ )
174
+ return
175
+
176
+ self.console.print(
177
+ Panel(
178
+ f"[bold]Model:[/bold] {selected.get('name', '—')}\n"
179
+ f"[bold]Path:[/bold] {model_path}\n\n"
180
+ "[dim]Type /bye to exit, /view_settings to inspect the model.[/dim]",
181
+ title="Playground",
182
+ border_style="cyan",
183
+ expand=False,
184
+ )
185
+ )
186
+
187
+ from ..utilities.chat_playground import PlaygroundModel
188
+
189
+ bot = PlaygroundModel(model_path=model_path)
190
+ bot.chat()
191
+
84
192
  # ── Steps ───────────────────────────────────────────────────────────────
85
193
 
86
194
  def _print_banner(self) -> None:
@@ -317,7 +425,7 @@ class ModelForgeWizard:
317
425
  # ── Entry point ──────────────────────────────────────────────────────────────
318
426
 
319
427
  def main() -> None:
320
- """Entry point for the ``modelforge-nb`` console script."""
428
+ """Entry point for the ``modelforge-nb`` console script (notebook mode)."""
321
429
  try:
322
430
  import questionary # noqa: F401
323
431
  import rich # noqa: F401
@@ -330,7 +438,7 @@ def main() -> None:
330
438
  ' pip install "modelforge-finetuning[cli]"\n'
331
439
  )
332
440
  sys.exit(1)
333
- ModelForgeWizard().run()
441
+ ModelForgeWizard(notebook=True).run()
334
442
 
335
443
 
336
444
  if __name__ == "__main__":
@@ -211,7 +211,7 @@ class QLoRAStrategy:
211
211
  max_seq_length = 2048
212
212
 
213
213
  # QLoRA-optimized training arguments
214
- training_args = SFTConfig(
214
+ sft_kwargs = dict(
215
215
  output_dir=config.get("output_dir", "./checkpoints"),
216
216
  num_train_epochs=config.get("num_train_epochs", 1),
217
217
  per_device_train_batch_size=config.get("per_device_train_batch_size", 4),
@@ -227,7 +227,6 @@ class QLoRAStrategy:
227
227
  bf16=config.get("bf16", True), # Default True for CUDA; MPS overrides to False in training_service
228
228
  max_grad_norm=config.get("max_grad_norm", 0.3),
229
229
  max_steps=config.get("max_steps", -1),
230
- group_by_length=config.get("group_by_length", True),
231
230
  lr_scheduler_type=config.get("lr_scheduler_type", "cosine"),
232
231
  report_to="tensorboard",
233
232
  logging_dir=config.get("logging_dir", "./training_logs"),
@@ -245,12 +244,17 @@ class QLoRAStrategy:
245
244
  load_best_model_at_end=True if eval_dataset else False,
246
245
  metric_for_best_model="eval_loss" if eval_dataset else None,
247
246
  ddp_find_unused_parameters=False,
248
- use_cache=False,
249
247
  # Dataloader settings (MPS sets these to avoid multiprocessing issues)
250
248
  dataloader_num_workers=config.get("dataloader_num_workers", 0),
251
249
  dataloader_pin_memory=config.get("dataloader_pin_memory", True),
252
250
  )
253
251
 
252
+ # Unsloth's patched SFTConfig doesn't support group_by_length
253
+ if not config.get("_peft_already_applied"):
254
+ sft_kwargs["group_by_length"] = config.get("group_by_length", True)
255
+
256
+ training_args = SFTConfig(**sft_kwargs)
257
+
254
258
  # Build the PEFT config for SFTTrainer to apply LoRA
255
259
  # Skip if PEFT was already applied (e.g. by Unsloth provider)
256
260
  peft_config = None if config.get("_peft_already_applied") else self.get_peft_config(config)
@@ -193,7 +193,7 @@ class SFTStrategy:
193
193
  max_seq_length = 2048
194
194
 
195
195
  # Create SFTConfig with training arguments
196
- training_args = SFTConfig(
196
+ sft_kwargs = dict(
197
197
  output_dir=config.get("output_dir", "./checkpoints"),
198
198
  num_train_epochs=config.get("num_train_epochs", 1),
199
199
  per_device_train_batch_size=config.get("per_device_train_batch_size", 1),
@@ -209,7 +209,6 @@ class SFTStrategy:
209
209
  bf16=config.get("bf16", False),
210
210
  max_grad_norm=config.get("max_grad_norm", 0.3),
211
211
  max_steps=config.get("max_steps", -1),
212
- group_by_length=config.get("group_by_length", True),
213
212
  lr_scheduler_type=config.get("lr_scheduler_type", "cosine"),
214
213
  report_to="tensorboard",
215
214
  logging_dir=config.get("logging_dir", "./training_logs"),
@@ -229,6 +228,12 @@ class SFTStrategy:
229
228
  dataloader_pin_memory=config.get("dataloader_pin_memory", True),
230
229
  )
231
230
 
231
+ # Unsloth's patched SFTConfig doesn't support group_by_length
232
+ if not config.get("_peft_already_applied"):
233
+ sft_kwargs["group_by_length"] = config.get("group_by_length", True)
234
+
235
+ training_args = SFTConfig(**sft_kwargs)
236
+
232
237
  # Build the PEFT config for SFTTrainer to apply LoRA
233
238
  # Skip if PEFT was already applied (e.g. by Unsloth provider)
234
239
  peft_config = None if config.get("_peft_already_applied") else self.get_peft_config(config)
@@ -1,22 +1,42 @@
1
1
  """
2
2
  Strategy factory for creating training strategy instances.
3
+
4
+ Strategy imports are deferred to avoid pulling in heavy dependencies (trl, wandb)
5
+ at module load time — which would break lightweight entry points like the notebook
6
+ CLI wizard that only need HardwareService.
3
7
  """
4
- from .sft_strategy import SFTStrategy
5
- from .rlhf_strategy import RLHFStrategy
6
- from .dpo_strategy import DPOStrategy
7
- from .qlora_strategy import QLoRAStrategy
8
8
  from ..exceptions import ConfigurationError
9
9
  from ..logging_config import logger
10
10
 
11
11
 
12
+ def _import_sft():
13
+ from .sft_strategy import SFTStrategy
14
+ return SFTStrategy
15
+
16
+
17
+ def _import_rlhf():
18
+ from .rlhf_strategy import RLHFStrategy
19
+ return RLHFStrategy
20
+
21
+
22
+ def _import_dpo():
23
+ from .dpo_strategy import DPOStrategy
24
+ return DPOStrategy
25
+
26
+
27
+ def _import_qlora():
28
+ from .qlora_strategy import QLoRAStrategy
29
+ return QLoRAStrategy
30
+
31
+
12
32
  class StrategyFactory:
13
33
  """Factory for creating training strategy instances."""
14
34
 
15
- _strategies = {
16
- "sft": SFTStrategy,
17
- "rlhf": RLHFStrategy,
18
- "dpo": DPOStrategy,
19
- "qlora": QLoRAStrategy,
35
+ _strategy_loaders = {
36
+ "sft": _import_sft,
37
+ "rlhf": _import_rlhf,
38
+ "dpo": _import_dpo,
39
+ "qlora": _import_qlora,
20
40
  }
21
41
 
22
42
  @classmethod
@@ -37,13 +57,13 @@ class StrategyFactory:
37
57
 
38
58
  strategy_name = strategy_name.lower()
39
59
 
40
- if strategy_name not in cls._strategies:
60
+ if strategy_name not in cls._strategy_loaders:
41
61
  raise ConfigurationError(
42
62
  f"Unknown training strategy: {strategy_name}. "
43
- f"Available strategies: {list(cls._strategies.keys())}"
63
+ f"Available strategies: {list(cls._strategy_loaders.keys())}"
44
64
  )
45
65
 
46
- strategy_class = cls._strategies[strategy_name]
66
+ strategy_class = cls._strategy_loaders[strategy_name]()
47
67
  return strategy_class()
48
68
 
49
69
  @classmethod
@@ -54,7 +74,7 @@ class StrategyFactory:
54
74
  Returns:
55
75
  List of strategy names
56
76
  """
57
- return list(cls._strategies.keys())
77
+ return list(cls._strategy_loaders.keys())
58
78
 
59
79
  @classmethod
60
80
  def register_strategy(cls, name: str, strategy_class):
@@ -66,4 +86,4 @@ class StrategyFactory:
66
86
  strategy_class: Strategy class
67
87
  """
68
88
  logger.info(f"Registering strategy: {name}")
69
- cls._strategies[name.lower()] = strategy_class
89
+ cls._strategy_loaders[name.lower()] = lambda _cls=strategy_class: _cls
@@ -78,7 +78,7 @@ class PlaygroundModel:
78
78
  input("\nPress Enter to exit...")
79
79
  exit(1)
80
80
  except KeyError:
81
- print(f"Pipeline task {self.modelforge_config['pipeline_task']} not found in definitions for the pipeline object of transformers.")
81
+ print(traceback.format_exc())
82
82
  input("\nPress Enter to exit...")
83
83
  exit(1)
84
84
  except Exception as e:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelforge-finetuning
3
- Version: 3.0.3
3
+ Version: 3.0.4
4
4
  Summary: ModelForge: A no-code toolkit for fine-tuning HuggingFace models
5
5
  Author-email: R3tr0 M1ll3r <r3tr0.m1ll3r@gmail.com>
6
6
  Project-URL: Documentation, https://modelforge-finetuning.readthedocs.io
@@ -36,6 +36,9 @@ Requires-Dist: questionary>=2.0.1; extra == "cli"
36
36
  Requires-Dist: rich>=13.0.0; extra == "cli"
37
37
  Requires-Dist: ipywidgets>=8.0.0; extra == "cli"
38
38
  Requires-Dist: ipython>=8.0.0; extra == "cli"
39
+ Provides-Extra: unsloth
40
+ Requires-Dist: unsloth; extra == "unsloth"
41
+ Requires-Dist: bitsandbytes==0.45.3; extra == "unsloth"
39
42
  Dynamic: license-file
40
43
 
41
44
  # ModelForge 🔧⚡
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelforge-finetuning
3
- Version: 3.0.3
3
+ Version: 3.0.4
4
4
  Summary: ModelForge: A no-code toolkit for fine-tuning HuggingFace models
5
5
  Author-email: R3tr0 M1ll3r <r3tr0.m1ll3r@gmail.com>
6
6
  Project-URL: Documentation, https://modelforge-finetuning.readthedocs.io
@@ -36,6 +36,9 @@ Requires-Dist: questionary>=2.0.1; extra == "cli"
36
36
  Requires-Dist: rich>=13.0.0; extra == "cli"
37
37
  Requires-Dist: ipywidgets>=8.0.0; extra == "cli"
38
38
  Requires-Dist: ipython>=8.0.0; extra == "cli"
39
+ Provides-Extra: unsloth
40
+ Requires-Dist: unsloth; extra == "unsloth"
41
+ Requires-Dist: bitsandbytes==0.45.3; extra == "unsloth"
39
42
  Dynamic: license-file
40
43
 
41
44
  # ModelForge 🔧⚡
@@ -27,3 +27,7 @@ ipython>=8.0.0
27
27
 
28
28
  [quantization]
29
29
  bitsandbytes==0.45.3
30
+
31
+ [unsloth]
32
+ unsloth
33
+ bitsandbytes==0.45.3
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "modelforge-finetuning"
3
- version = "3.0.3"
3
+ version = "3.0.4"
4
4
  description = "ModelForge: A no-code toolkit for fine-tuning HuggingFace models"
5
5
  authors = [
6
6
  {name = "R3tr0 M1ll3r", email = "r3tr0.m1ll3r@gmail.com"},
@@ -40,6 +40,10 @@ cli = [
40
40
  "ipywidgets>=8.0.0",
41
41
  "ipython>=8.0.0",
42
42
  ]
43
+ unsloth = [
44
+ "unsloth",
45
+ "bitsandbytes==0.45.3",
46
+ ]
43
47
 
44
48
  [tool.setuptools.packages.find]
45
49
  where = ["."]