codefinetuner 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,136 @@
1
+ import argparse
2
+ import logging.config
3
+ import shutil
4
+ import sys
5
+ from typing import Tuple
6
+ from pathlib import Path
7
+
8
+ from datasets import Features, IterableDataset, Sequence, Value, load_dataset
9
+ from transformers import AutoTokenizer
10
+
11
+ from .config import Config
12
+ from .model import load_and_configure_lora_model
13
+ from .train import plot_loss, save_log, train_lora_model, merge_lora_and_save
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _setup_logger(log_level: str) -> None:
20
+ """
21
+ Configure the root logger ("") to capture logs from the entry point (__main__),
22
+ all internal sub-modules, and third-party libraries via a single handler.
23
+ """
24
+ logger_config = {
25
+ "version": 1,
26
+ "disable_existing_loggers": False,
27
+ "formatters": {
28
+ "standard": {
29
+ "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
30
+ }
31
+ },
32
+ "handlers": {
33
+ "stderr_handler": {
34
+ "level": log_level,
35
+ "class": "logging.StreamHandler",
36
+ "formatter": "standard",
37
+ }
38
+ },
39
+ "loggers": {
40
+ "": { # "" corresponds to root logger
41
+ "handlers": ["stderr_handler"],
42
+ "level": log_level,
43
+ "propagate": False,
44
+ }
45
+ }
46
+ }
47
+ logging.config.dictConfig(logger_config)
48
+
49
+
50
+ def _parse_args() -> argparse.Namespace:
51
+ parser = argparse.ArgumentParser(description="Start or resume LoRA model training")
52
+ parser.add_argument(
53
+ "--config",
54
+ type=Path,
55
+ required=True,
56
+ help="YAML config file path with 'finetune:' section. All Config.MISSING fields must be provided."
57
+ )
58
+ parser.add_argument(
59
+ "--log-level",
60
+ type=str,
61
+ default="INFO",
62
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
63
+ help="Logging level.",
64
+ )
65
+ args = parser.parse_args()
66
+ return args
67
+
68
+
69
+ def _ensure_clean_checkpoint_dir(config: Config) -> None:
70
+ checkpoints_dir = config.trainer_checkpoints_dir_path
71
+ checkpoint = config.trainer_resume_from_checkpoint
72
+ clear = config.trainer_clear_checkpoint_dir
73
+ if checkpoint and not clear:
74
+ logger.info(f"Resuming from: {checkpoint}")
75
+ elif not checkpoint and clear:
76
+ if checkpoints_dir.exists():
77
+ logger.warning(f"Deleting: {checkpoints_dir}")
78
+ shutil.rmtree(checkpoints_dir)
79
+ checkpoints_dir.mkdir(parents=True, exist_ok=True)
80
+ logger.info(f"Cleared: {checkpoints_dir}")
81
+ elif checkpoint and clear:
82
+ raise RuntimeError("Configuration conflict: Cannot resume from a checkpoint while '--delete-all-checkpoints' is set.")
83
+ else:
84
+ logger.info(f"Starting fresh training. Existing checkpoints in {checkpoints_dir} are preserved.")
85
+
86
+
87
+ def load_datasets(config: Config) -> Tuple[IterableDataset, IterableDataset]:
88
+ # Define the expected schema/features of datasets.
89
+ # Use 'int32' which the datasets library and pytorch map correctly to int tensors.
90
+ dataset_features = Features({
91
+ 'input_ids': Sequence(feature=Value(dtype='int32')),
92
+ 'attention_mask': Sequence(feature=Value(dtype='int32')),
93
+ 'labels': Sequence(feature=Value(dtype='int32')),
94
+ })
95
+
96
+ # Enable streaming mode to load the dataset as an iterator.
97
+ # This allows processing data samples on-the-fly without downloading or loading the entire dataset into memory.
98
+ # https://huggingface.co/docs/datasets/stream
99
+ train_dataset = load_dataset("json", data_files=str(config.train_dataset_path), features=dataset_features, streaming=True)["train"]
100
+ train_dataset = train_dataset.shuffle(buffer_size=config.dataset_shuffle_buffer_size, seed=config.dataset_shuffle_seed) # take up to suffle_buffer_size examples and randomly shuffle them
101
+ eval_dataset = load_dataset("json", data_files=str(config.eval_dataset_path), features=dataset_features, streaming=True)["train"]
102
+ return train_dataset, eval_dataset
103
+
104
+
105
+ def run(config: Config) -> None:
106
+ _ensure_clean_checkpoint_dir(config)
107
+
108
+ train_dataset, eval_dataset= load_datasets(config)
109
+ logger.info(f"Dataset: {config.dataset_train_dataset_length} train examples, max_steps={config.trainer_max_steps}")
110
+
111
+ lora_model = load_and_configure_lora_model(config)
112
+ lora_model.print_trainable_parameters()
113
+
114
+ tokenizer = AutoTokenizer.from_pretrained(config.model_name)
115
+ tokenizer.pad_token = config.fim_pad_token
116
+ tokenizer.padding_side = "right"
117
+
118
+ log_history = train_lora_model(config, lora_model, tokenizer, train_dataset, eval_dataset)
119
+ merge_lora_and_save(config, tokenizer)
120
+ save_log(config, log_history)
121
+
122
+ plot_loss(config)
123
+
124
+
125
+ def main() -> None:
126
+ user_args = _parse_args()
127
+ _setup_logger(user_args.log_level)
128
+ try:
129
+ finetune_config = Config.load_from_yaml(user_args.config)
130
+ run(finetune_config)
131
+ except Exception as e:
132
+ logger.exception(f"Finetuning failed")
133
+ sys.exit(1)
134
+
135
+ if __name__ == "__main__":
136
+ main()
@@ -0,0 +1,204 @@
1
+ import gc
2
+ import json
3
+ import logging
4
+ import shutil
5
+ from pathlib import Path
6
+ from typing import Dict, List
7
+
8
+ import matplotlib.pyplot as plt
9
+ import torch
10
+ from datasets import IterableDataset
11
+ from peft import PeftModel
12
+ from transformers import AutoModelForCausalLM, AutoTokenizer, Trainer, TrainingArguments
13
+
14
+ from .config import Config
15
+
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class FIMDataCollator:
21
+ def __init__(self, tokenizer, label_pad_token_id):
22
+ self.tokenizer = tokenizer
23
+ self.label_pad_token_id = label_pad_token_id
24
+
25
+ def __call__(self, examples: List[Dict[str, List[int]]]) -> Dict[str, torch.Tensor]:
26
+ # find max length for this specific batch
27
+ max_ex_length = 0
28
+ for ex in examples:
29
+ if len(ex["input_ids"]) > max_ex_length:
30
+ max_ex_length = len(ex["input_ids"])
31
+
32
+ # apply padding
33
+ padded_examples = []
34
+ for ex in examples:
35
+ pad_length = max_ex_length - len(ex["input_ids"])
36
+ padded_ex = {
37
+ "input_ids": (ex["input_ids"] + [self.tokenizer.pad_token_id] * pad_length),
38
+ "attention_mask": (ex["attention_mask"] + [0] * pad_length),
39
+ "labels": (ex["labels"] + [self.label_pad_token_id] * pad_length)
40
+ }
41
+ padded_examples.append(padded_ex)
42
+
43
+ # stack separate examples into a single tensors matrix
44
+ examples_batch = {
45
+ "input_ids": torch.tensor([ex["input_ids"] for ex in padded_examples], dtype=torch.long),
46
+ "attention_mask": torch.tensor([ex["attention_mask"] for ex in padded_examples], dtype=torch.long),
47
+ "labels": torch.tensor([ex["labels"] for ex in padded_examples], dtype=torch.long)
48
+ }
49
+
50
+ return examples_batch
51
+
52
+
53
+ def train_lora_model(
54
+ config: Config,
55
+ lora_model: PeftModel,
56
+ tokenizer: AutoTokenizer,
57
+ train_dataset: IterableDataset,
58
+ eval_dataset: IterableDataset,
59
+ ) -> List:
60
+ logger.info(f"Starting training: {config.trainer_max_steps} steps on {config.device}, batch_size={config.trainer_per_device_train_batch_size}")
61
+
62
+ if config.model_dtype == torch.bfloat16:
63
+ trainer_bf16 = True
64
+ trainer_fp16 = False
65
+ elif config.model_dtype == torch.float16:
66
+ trainer_bf16 = False
67
+ trainer_fp16 = True
68
+ else:
69
+ trainer_bf16 = False
70
+ trainer_fp16 = False
71
+
72
+ training_args = TrainingArguments(
73
+ output_dir=config.trainer_checkpoints_dir_path,
74
+ per_device_train_batch_size=config.trainer_per_device_train_batch_size,
75
+ per_device_eval_batch_size=config.trainer_per_device_eval_batch_size,
76
+ gradient_accumulation_steps=config.trainer_gradient_accumulation_steps,
77
+ learning_rate=config.trainer_learning_rate,
78
+ weight_decay=config.trainer_weight_decay,
79
+ max_grad_norm=config.trainer_max_grad_norm,
80
+ max_steps=config.trainer_max_steps,
81
+ lr_scheduler_type=config.trainer_lr_scheduler_type,
82
+ warmup_steps=config.trainer_warmup_steps,
83
+ eval_strategy=config.trainer_eval_strategy,
84
+ logging_steps=config.trainer_logging_steps,
85
+ eval_steps=config.trainer_eval_steps,
86
+ logging_strategy=config.trainer_logging_strategy,
87
+ save_strategy=config.trainer_save_strategy,
88
+ save_steps=config.trainer_save_steps,
89
+ bf16=trainer_bf16,
90
+ fp16=trainer_fp16,
91
+ gradient_checkpointing=config.trainer_gradient_checkpointing
92
+ )
93
+
94
+ data_collator = FIMDataCollator(
95
+ tokenizer=tokenizer,
96
+ label_pad_token_id=config.label_pad_token_id
97
+ )
98
+
99
+ trainer = Trainer(
100
+ model=lora_model,
101
+ args=training_args,
102
+ train_dataset=train_dataset,
103
+ eval_dataset=eval_dataset,
104
+ processing_class = tokenizer,
105
+ data_collator = data_collator
106
+ )
107
+
108
+ checkpoint = config.trainer_resume_from_checkpoint
109
+ if checkpoint == "last":
110
+ trainer.train(resume_from_checkpoint=True)
111
+ elif checkpoint is not None:
112
+ trainer.train(resume_from_checkpoint=checkpoint)
113
+ else:
114
+ trainer.train() # train from scratch
115
+
116
+ lora_model.save_pretrained(config.lora_adapter_path) # save lora adapter only
117
+ log_history = trainer.state.log_history
118
+
119
+ del trainer
120
+ del lora_model
121
+ return log_history
122
+
123
+
124
+ def merge_lora_and_save(config: Config, tokenizer: AutoTokenizer) -> None:
125
+ gc.collect() # force garbage collection
126
+
127
+ if torch.cuda.is_available():
128
+ torch.cuda.synchronize() # wait for gpu
129
+ torch.cuda.empty_cache() # clear gpu cache
130
+
131
+ # ensure offload folder for merging exists before loading the model
132
+ config.trainer_model_merge_offload_folder_path.mkdir(parents=True, exist_ok=True)
133
+
134
+ # load fresh base model
135
+ base_model = AutoModelForCausalLM.from_pretrained(
136
+ pretrained_model_name_or_path=config.model_name,
137
+ dtype=config.model_dtype,
138
+ device_map="auto",
139
+ offload_folder=str(config.trainer_model_merge_offload_folder_path), # offload model layers to disk during loading to prevent RAM (OOM) crashes
140
+ low_cpu_mem_usage=True
141
+ )
142
+
143
+ lora_model = PeftModel.from_pretrained(
144
+ model=base_model,
145
+ model_id=config.lora_adapter_path,
146
+ offload_folder=str(config.trainer_model_merge_offload_folder_path)
147
+ )
148
+
149
+ # merge lora adapter into base model and save it with the tokenizer of the model
150
+ merged_model= lora_model.merge_and_unload()
151
+ merged_model = merged_model.to(config.model_dtype)
152
+ merged_model.save_pretrained(config.lora_model_path)
153
+ tokenizer.save_pretrained(config.lora_model_path)
154
+
155
+ # clean up offload folder
156
+ if config.trainer_model_merge_offload_folder_path.exists():
157
+ shutil.rmtree(config.trainer_model_merge_offload_folder_path)
158
+
159
+
160
+ def save_log(config: Config, log_history: List) -> None:
161
+ history = {
162
+ "train": {"steps": [], "loss": [], "learning_rate": [], "epoch": []},
163
+ "eval": {"steps": [], "loss": [], "epoch": []}
164
+ }
165
+
166
+ for entry in log_history:
167
+ # Training logs
168
+ if "loss" in entry:
169
+ history["train"]["loss"].append(entry["loss"])
170
+ history["train"]["steps"].append(entry["step"])
171
+ history["train"]["epoch"].append(entry.get("epoch"))
172
+ history["train"]["learning_rate"].append(entry.get("learning_rate"))
173
+
174
+ # Evaluation logs
175
+ elif "eval_loss" in entry:
176
+ history["eval"]["loss"].append(entry["eval_loss"])
177
+ history["eval"]["steps"].append(entry["step"])
178
+ history["eval"]["epoch"].append(entry.get("epoch"))
179
+
180
+ log_path = config.trainer_log_path
181
+ with log_path.open("w", encoding="utf-8") as f:
182
+ json.dump(history, f, indent=2)
183
+
184
+
185
+ def plot_loss(config: Config) -> None:
186
+ log_path = config.trainer_log_path
187
+ with log_path.open("r", encoding="utf-8") as f:
188
+ data = json.load(f)
189
+
190
+ plt.figure(figsize=(8, 5))
191
+
192
+ if data["train"]["steps"]:
193
+ plt.plot(data["train"]["steps"], data["train"]["loss"], label="Train Loss")
194
+
195
+ if data["eval"]["steps"]:
196
+ plt.plot(data["eval"]["steps"], data["eval"]["loss"], label="Eval Loss", marker='o')
197
+
198
+ plt.xlabel("Step")
199
+ plt.ylabel("Loss")
200
+ plt.title("Training and Evaluation Loss")
201
+ plt.legend()
202
+ plt.grid(True)
203
+ plt.savefig(config.trainer_plot_path)
204
+ plt.close()
@@ -0,0 +1,152 @@
1
+ import argparse
2
+ import logging
3
+ import logging.config
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from .preprocess.config import Config as PreprocessConfig
8
+ from .preprocess.run import run as preprocess_run
9
+ from .finetune.config import Config as FinetuneConfig
10
+ from .finetune.run import run as finetune_run
11
+ from .evaluate.config import Config as EvaluateConfig
12
+ from .evaluate.run import run as evaluate_run
13
+ from .convert.config import Config as ConvertConfig
14
+ from .convert.run import run as convert_run
15
+
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def _setup_logger(log_level: str) -> None:
21
+ """
22
+ Configure the root logger ("") to capture logs from the entry point (__main__),
23
+ all internal sub-modules, and third-party libraries via a single handler.
24
+ """
25
+ logger_config = {
26
+ "version": 1,
27
+ "disable_existing_loggers": False,
28
+ "formatters": {
29
+ "standard": {
30
+ "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
31
+ }
32
+ },
33
+ "handlers": {
34
+ "stderr_handler": {
35
+ "level": log_level,
36
+ "class": "logging.StreamHandler",
37
+ "formatter": "standard",
38
+ }
39
+ },
40
+ "loggers": {
41
+ "": { # "" corresponds to root logger
42
+ "handlers": ["stderr_handler"],
43
+ "level": log_level,
44
+ "propagate": False,
45
+ }
46
+ },
47
+ }
48
+ logging.config.dictConfig(logger_config)
49
+
50
+
51
+ def _parse_args() -> argparse.Namespace:
52
+ parser = argparse.ArgumentParser(
53
+ description="Run full pipeline: preprocess -> finetune -> evaluate."
54
+ )
55
+ parser.add_argument(
56
+ "--config",
57
+ type=Path,
58
+ required=True,
59
+ help="YAML config file path with 'preprocess:', 'finetune:' and 'evaluate:' sections.",
60
+ )
61
+ parser.add_argument(
62
+ "--log-level",
63
+ type=str,
64
+ default="INFO",
65
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
66
+ help="Logging level for the pipeline entrypoint.",
67
+ )
68
+ parser.add_argument(
69
+ "--skip-preprocess",
70
+ action="store_true",
71
+ help="Skip preprocess stage and reuse existing datasets.",
72
+ )
73
+ parser.add_argument(
74
+ "--skip-finetune",
75
+ action="store_true",
76
+ help="Skip finetune stage and reuse existing checkpoints.",
77
+ )
78
+ parser.add_argument(
79
+ "--skip-evaluate",
80
+ action="store_true",
81
+ help="Skip evaluation stage.",
82
+ )
83
+ parser.add_argument(
84
+ "--skip-convert",
85
+ action="store_true",
86
+ help="Skip conversion to gguf file stage.",
87
+ )
88
+ return parser.parse_args()
89
+
90
+
91
+ def run_pipeline(
92
+ config_path: Path,
93
+ skip_preprocess: bool = False,
94
+ skip_finetune: bool = False,
95
+ skip_evaluate: bool = False,
96
+ skip_convert: bool = False,
97
+ ) -> None:
98
+ logger.info("Starting end-to-end pipeline")
99
+
100
+ if not skip_preprocess:
101
+ logger.info("=== Stage 1/4: Preprocess ===")
102
+ preprocess_config = PreprocessConfig.load_from_yaml(config_path)
103
+ preprocess_run(preprocess_config)
104
+ logger.info("Finished preprocess stage")
105
+ else:
106
+ logger.info("Skipping preprocess stage")
107
+
108
+ if not skip_finetune:
109
+ logger.info("=== Stage 2/4: Finetune ===")
110
+ finetune_config = FinetuneConfig.load_from_yaml(config_path)
111
+ finetune_run(finetune_config)
112
+ logger.info("Finished finetune stage")
113
+ else:
114
+ logger.info("Skipping finetune stage")
115
+
116
+ if not skip_evaluate:
117
+ logger.info("=== Stage 3/4: Evaluate ===")
118
+ evaluate_config = EvaluateConfig.load_from_yaml(config_path)
119
+ evaluate_run(evaluate_config)
120
+ logger.info("Finished evaluate stage")
121
+ else:
122
+ logger.info("Skipping evaluate stage")
123
+
124
+ if not skip_convert:
125
+ logger.info("=== Stage 4/4: Convert ===")
126
+ convert_config = ConvertConfig.load_from_yaml(config_path)
127
+ convert_run(convert_config)
128
+ logger.info("Finished conversion stage")
129
+ else:
130
+ logger.info("Skipping conversion stage")
131
+
132
+ logger.info("Pipeline completed successfully")
133
+
134
+
135
+ def main() -> None:
136
+ user_args = _parse_args()
137
+ _setup_logger(user_args.log_level)
138
+ try:
139
+ run_pipeline(
140
+ config_path=user_args.config,
141
+ skip_preprocess=user_args.skip_preprocess,
142
+ skip_finetune=user_args.skip_finetune,
143
+ skip_evaluate=user_args.skip_evaluate,
144
+ skip_convert=user_args.skip_convert,
145
+ )
146
+ except Exception:
147
+ logger.exception("Pipeline execution failed")
148
+ sys.exit(1)
149
+
150
+
151
+ if __name__ == "__main__":
152
+ main()
File without changes
@@ -0,0 +1,160 @@
1
+ import logging
2
+ import math
3
+ import json
4
+ from importlib import resources
5
+ from dataclasses import dataclass, field, fields
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import numpy as np
10
+ from omegaconf import OmegaConf, MISSING
11
+
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @dataclass
17
+ class Config:
18
+ # --- Mandatory Parameters ---
19
+ model_name: str = MISSING
20
+ fim_prefix_token: str = MISSING
21
+ fim_middle_token: str = MISSING
22
+ fim_suffix_token: str = MISSING
23
+ fim_pad_token: str = MISSING
24
+ eos_token: str = MISSING
25
+ data_language: str = MISSING
26
+ data_extensions: list[str] = MISSING
27
+
28
+ # --- Preprocess Local Parameters ---
29
+ split_mode: str = "auto"
30
+ train_ratio: float = 0.8
31
+ eval_ratio: float = 0.1
32
+ test_ratio: float = 0.1
33
+ max_token_sequence_length: int = 1024 # used with bytes_per_token_ratio to convert bytes to tokens, final token count is thus not exact
34
+ max_code_blocks_ast_depth: int = 2 # depth 1 is root, 2 includes child nodes (e.g. functions)
35
+ min_middle_tokens_length: int = 20 # used with estimated bytes_per_token_ratio to convert bytes to tokens, final token count is thus not exact
36
+ max_middle_tokens_length: int = 200 # used with estimated bytes_per_token_ratio to convert bytes to tokens, final token count is thus not exact
37
+ fim_examples_per_subblock_ratio: float = 1.0 # 1.0 = all fim examples of a subblock are extracted, 0.5 = onls 50% of fim examples of a subblock are extracted
38
+ tokenizer_batch_size: int = 32
39
+ rng_seed: int = 0
40
+
41
+ # --- Tree Sitter Parser ---
42
+ tree_sitter_parser: Any = field(init=False) # type hint Any because omegaconf does not recognize ts.Parser as a valid type (yaml file cannot contain an object of ts.Parser)
43
+ tree_sitter_block_types: Any = field(init=False) # type hint Any because omegaconf does not support set type
44
+ tree_sitter_subblock_types: Any = field(init=False) # type hint Any because omegaconf does not support set type
45
+
46
+ # --- Paths ---
47
+ workspace_path: Path | None = None
48
+ raw_data_path: Path | None = None
49
+ train_dataset_path: Path = field(init=False)
50
+ eval_dataset_path: Path = field(init=False)
51
+ test_dataset_path: Path = field(init=False)
52
+ tree_sitter_parser_path: Path | None = None
53
+ tree_sitter_definitions_path: Path | None = None
54
+
55
+ # --- Randomization ---
56
+ rng: Any = field(init=False) # type hint Any because omegaconf does not support np.random.Generator type
57
+
58
+ @classmethod
59
+ def load_from_yaml(cls, yaml_path: Path) -> "Config":
60
+ if not yaml_path.exists():
61
+ raise FileNotFoundError(f"Configuration file not found at: {yaml_path}")
62
+
63
+ config_dict = OmegaConf.structured(cls)
64
+ try:
65
+ yaml_file_node = OmegaConf.load(yaml_path)
66
+ except Exception as e:
67
+ raise ValueError(f"Failed to parse YAML file: {yaml_path}") from e
68
+
69
+ yaml_file_dict = OmegaConf.to_container(yaml_file_node, resolve=True)
70
+ yaml_preprocess_dict = yaml_file_dict.get("preprocess", {})
71
+
72
+ yaml_preprocess_valid_dict = {}
73
+ # Filter YAML fields to include only those defined in the Config dataclass.
74
+ # This prevents OmegaConf from raising an AttributeError when encountering
75
+ # global YAML anchors or keys not present in the current Config dataclass.
76
+ for field in fields(cls):
77
+ if field.name in yaml_preprocess_dict:
78
+ yaml_preprocess_valid_dict[field.name] = yaml_preprocess_dict[field.name]
79
+ logger.debug(f"Filtered YAML configuration: {yaml_preprocess_valid_dict}")
80
+
81
+ merged_config_dict = OmegaConf.merge(config_dict, yaml_preprocess_valid_dict)
82
+
83
+ return OmegaConf.to_object(merged_config_dict)
84
+
85
+ def __post_init__(self) -> None:
86
+ self._validate_ratio()
87
+ self._setup_paths()
88
+ self._ensure_output_paths_exist()
89
+ self._load_language_blocks()
90
+ self._init_tree_sitter_parser()
91
+ self.rng = np.random.default_rng(seed=self.rng_seed)
92
+ logger.debug("Config initialization complete.")
93
+
94
+ def _validate_ratio(self):
95
+ total_ratio = self.train_ratio + self.eval_ratio + self.test_ratio
96
+ if not math.isclose(total_ratio, 1.0, rel_tol=1e-6):
97
+ raise ValueError(f"Train + eval + test ratios must sum to 1.0, got {total_ratio}")
98
+
99
+ def _setup_paths(self) -> None:
100
+ if self.workspace_path is None:
101
+ self.workspace_path = Path.cwd()
102
+ if self.raw_data_path is None:
103
+ self.raw_data_path = self.workspace_path / "data"
104
+
105
+ if self.tree_sitter_definitions_path is None:
106
+ try:
107
+ self.tree_sitter_definitions_path = Path(resources.files(__package__)/ "tree_sitter_definitions.json")
108
+ except Exception:
109
+ raise FileNotFoundError("Missing internal tree_sitter_definitions.json")
110
+
111
+ self.preprocess_outputs_dir_path = self.workspace_path / "outputs" / "preprocess"
112
+ self.train_dataset_path = self.preprocess_outputs_dir_path / "results" / "datasets" / "train_dataset.jsonl"
113
+ self.eval_dataset_path = self.preprocess_outputs_dir_path / "results" / "datasets" / "eval_dataset.jsonl"
114
+ self.test_dataset_path = self.preprocess_outputs_dir_path / "results" / "datasets" / "test_dataset.jsonl"
115
+ logger.debug(f"Resolved workspace path to: {self.workspace_path}")
116
+
117
+ def _ensure_output_paths_exist(self) -> None:
118
+ paths = [
119
+ self.preprocess_outputs_dir_path,
120
+ self.train_dataset_path,
121
+ self.eval_dataset_path,
122
+ self.test_dataset_path
123
+ ]
124
+
125
+ for path in paths:
126
+ if not path.parent.exists():
127
+ path.parent.mkdir(parents=True, exist_ok=True)
128
+ logger.debug(f"Created parent directory: {path.parent}")
129
+ else:
130
+ logger.debug(f"Parent directory already exists: {path.parent}")
131
+
132
+ def _load_language_blocks(self) -> None:
133
+ blocks_path = self.tree_sitter_definitions_path
134
+ with open(blocks_path, "r", encoding="utf-8") as f:
135
+ language_data = json.load(f)
136
+
137
+ language_blocks = language_data.get(self.data_language)
138
+ if language_blocks is None:
139
+ raise ValueError(f"Language '{self.data_language}' not found in {blocks_path}")
140
+
141
+ tree_sitter_block_types = language_blocks.get("block_types")
142
+ tree_sitter_subblock_types = language_blocks.get("subblock_types")
143
+ if not isinstance(tree_sitter_block_types, list) or not isinstance(tree_sitter_subblock_types, list):
144
+ raise ValueError(f"Invalid block definitions for '{self.data_language}' in {blocks_path}")
145
+
146
+ self.tree_sitter_block_types = set(tree_sitter_block_types)
147
+ self.tree_sitter_subblock_types = set(tree_sitter_subblock_types)
148
+
149
+ def _init_tree_sitter_parser(self) -> None:
150
+ if self.tree_sitter_parser_path:
151
+ logger.info(f"Loading custom tree-sitter parser from {self.tree_sitter_parser_path}")
152
+ from .extract import get_custom_tree_sitter_parser
153
+ self.tree_sitter_parser = get_custom_tree_sitter_parser(self.tree_sitter_parser_path, self.data_language)
154
+ else:
155
+ logger.info(f"Loading tree-sitter language pack parser {self.data_language}")
156
+ from .extract import get_tree_sitter_language_pack_parser
157
+ self.tree_sitter_parser = get_tree_sitter_language_pack_parser(self.data_language)
158
+
159
+ if self.tree_sitter_parser is None:
160
+ raise RuntimeError("Tree-sitter parser not initialized")