vectorplasticity commited on
Commit
8a2d8a9
·
verified ·
1 Parent(s): 352ecc9

Add comprehensive dataset column mapping and prompt template controls

Browse files
Files changed (1) hide show
  1. app/routers/training.py +659 -19
app/routers/training.py CHANGED
@@ -1,14 +1,16 @@
1
  """
2
  Training API Router - Endpoints for model training
 
3
  """
4
 
5
  from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends
6
  from fastapi.responses import JSONResponse
7
  from pydantic import BaseModel, Field, validator
8
- from typing import Optional, Dict, List, Any
9
  from datetime import datetime
10
  import uuid
11
  import logging
 
12
 
13
  from app.services.queue_service import TrainingQueue, JobPriority
14
  from app.database import get_db, TrainingJob, JobStatus, TaskType
@@ -20,23 +22,427 @@ logger = logging.getLogger(__name__)
20
  router = APIRouter(prefix="/api/training", tags=["Training"])
21
 
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  class DatasetConfig(BaseModel):
24
- """Dataset configuration."""
25
- source: str = Field(default="huggingface", description="Dataset source: huggingface, local, or upload")
 
26
  name: Optional[str] = Field(None, description="HuggingFace dataset name")
27
- config: Optional[str] = Field(None, description="Dataset config/subset")
28
- split: str = Field(default="train", description="Dataset split")
29
- text_column: Optional[str] = Field(None, description="Column name for text data")
30
- label_column: Optional[str] = Field(None, description="Column name for labels")
31
- source_column: Optional[str] = Field(None, description="Source column for seq2seq")
32
- target_column: Optional[str] = Field(None, description="Target column for seq2seq")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  max_length: int = Field(default=512, description="Max sequence length")
34
  max_target_length: int = Field(default=128, description="Max target length for seq2seq")
35
- validation_split: float = Field(default=0.1, description="Validation split ratio")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  path: Optional[str] = Field(None, description="Local file path")
37
- type: Optional[str] = Field(None, description="Local file type (json, csv, text)")
 
 
 
 
38
 
39
 
 
 
 
 
40
  class TrainingArgsConfig(BaseModel):
41
  """Training arguments configuration."""
42
  epochs: int = Field(default=3, ge=1, le=100)
@@ -56,18 +462,44 @@ class TrainingArgsConfig(BaseModel):
56
  bf16: bool = Field(default=False)
57
  gradient_checkpointing: bool = Field(default=False)
58
  optimizer: str = Field(default="adamw_torch")
 
59
  report_to: str = Field(default="none")
60
  seed: int = Field(default=42)
 
 
 
 
 
 
 
 
 
 
 
61
 
62
 
63
  class PEFTConfig(BaseModel):
64
  """PEFT/LoRA configuration."""
65
  enabled: bool = Field(default=True)
 
66
  r: int = Field(default=16, ge=1, le=256, description="LoRA rank")
67
  alpha: int = Field(default=32, ge=1, description="LoRA alpha")
68
  dropout: float = Field(default=0.05, ge=0.0, le=0.5)
69
  target_modules: List[str] = Field(default=["q_proj", "v_proj"])
70
  bias: str = Field(default="none")
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
 
73
  class OutputConfig(BaseModel):
@@ -75,10 +507,12 @@ class OutputConfig(BaseModel):
75
  push_to_hub: bool = Field(default=False)
76
  hub_model_id: Optional[str] = Field(None)
77
  private: bool = Field(default=False)
 
 
78
 
79
 
80
  class TrainingRequest(BaseModel):
81
- """Full training request."""
82
  name: str = Field(..., min_length=1, max_length=255)
83
  description: Optional[str] = Field(None)
84
  task_type: str = Field(default="causal-lm")
@@ -96,7 +530,8 @@ class TrainingRequest(BaseModel):
96
  "causal-lm", "seq2seq", "token-classification",
97
  "sequence-classification", "question-answering",
98
  "summarization", "translation", "text-classification",
99
- "masked-lm", "vision-classification", "audio-classification"
 
100
  ]
101
  if v not in valid_types:
102
  raise ValueError(f"Invalid task_type. Must be one of: {valid_types}")
@@ -132,6 +567,18 @@ class JobStatusResponse(BaseModel):
132
  hub_model_id: Optional[str]
133
 
134
 
 
 
 
 
 
 
 
 
 
 
 
 
135
  # Global queue instance
136
  training_queue = None
137
 
@@ -144,6 +591,203 @@ def get_queue():
144
  return training_queue
145
 
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  @router.post("/start", response_model=TrainingJobResponse)
148
  async def start_training(
149
  request: TrainingRequest,
@@ -164,7 +808,7 @@ async def start_training(
164
  dataset_source=request.dataset.source,
165
  dataset_name=request.dataset.name,
166
  dataset_config=request.dataset.config,
167
- dataset_split=request.dataset.split,
168
  training_args=request.training_args.dict(),
169
  peft_config=request.peft_config.dict() if request.peft_config else None,
170
  status=JobStatus.PENDING.value,
@@ -175,7 +819,7 @@ async def start_training(
175
  db.add(training_job)
176
  await db.commit()
177
 
178
- # Build config
179
  config = {
180
  "job_id": job_id,
181
  "task_type": request.task_type,
@@ -210,11 +854,9 @@ async def get_job_status(
210
  db: AsyncSession = Depends(get_db)
211
  ):
212
  """Get status of a training job."""
213
- # Check queue first
214
  queue = get_queue()
215
  queue_status = await queue.get_status(job_id)
216
 
217
- # Get from database
218
  result = await db.execute(
219
  select(TrainingJob).where(TrainingJob.job_id == job_id)
220
  )
@@ -223,7 +865,6 @@ async def get_job_status(
223
  if not job:
224
  raise HTTPException(status_code=404, detail="Job not found")
225
 
226
- # Merge queue status with database status
227
  if queue_status:
228
  job.status = queue_status.get("status", job.status)
229
  if queue_status.get("progress"):
@@ -304,7 +945,6 @@ async def cancel_job(
304
  if not cancelled:
305
  raise HTTPException(status_code=400, detail="Cannot cancel job")
306
 
307
- # Update database
308
  result = await db.execute(
309
  select(TrainingJob).where(TrainingJob.job_id == job_id)
310
  )
 
1
  """
2
  Training API Router - Endpoints for model training
3
+ Enhanced with dataset column mapping and prompt structure controls
4
  """
5
 
6
  from fastapi import APIRouter, HTTPException, BackgroundTasks, Depends
7
  from fastapi.responses import JSONResponse
8
  from pydantic import BaseModel, Field, validator
9
+ from typing import Optional, Dict, List, Any, Union
10
  from datetime import datetime
11
  import uuid
12
  import logging
13
+ import re
14
 
15
  from app.services.queue_service import TrainingQueue, JobPriority
16
  from app.database import get_db, TrainingJob, JobStatus, TaskType
 
22
  router = APIRouter(prefix="/api/training", tags=["Training"])
23
 
24
 
25
+ # ============================================
26
+ # PROMPT TEMPLATE CONFIGURATION
27
+ # ============================================
28
+
29
+ class PromptSectionConfig(BaseModel):
30
+ """Configuration for a single prompt section (system, user, assistant, etc.)"""
31
+ enabled: bool = Field(default=True, description="Whether to include this section")
32
+ template: str = Field(
33
+ default="",
34
+ description="Template string with {column} placeholders"
35
+ )
36
+ columns: List[str] = Field(
37
+ default_factory=list,
38
+ description="List of dataset columns used in this section"
39
+ )
40
+ prefix: str = Field(default="", description="Prefix before content (e.g., 'System: ')")
41
+ suffix: str = Field(default="", description="Suffix after content (e.g., '\n\n')")
42
+ strip_whitespace: bool = Field(default=True)
43
+ required: bool = Field(default=False, description="Raise error if columns missing")
44
+
45
+
46
+ class PromptTemplateConfig(BaseModel):
47
+ """Full prompt template configuration for training data formatting"""
48
+ # Chat-style formatting
49
+ use_chat_format: bool = Field(default=True, description="Use chat-style message format")
50
+ chat_template: Optional[str] = Field(
51
+ default=None,
52
+ description="Jinja2 chat template (auto-detect from tokenizer if None)"
53
+ )
54
+
55
+ # Message sections
56
+ system: Optional[PromptSectionConfig] = Field(
57
+ default=None,
58
+ description="System message configuration"
59
+ )
60
+ user: Optional[PromptSectionConfig] = Field(
61
+ default=None,
62
+ description="User message configuration"
63
+ )
64
+ context: Optional[PromptSectionConfig] = Field(
65
+ default=None,
66
+ description="Context/passages configuration"
67
+ )
68
+ reasoning: Optional[PromptSectionConfig] = Field(
69
+ default=None,
70
+ description="Reasoning/chain-of-thought configuration"
71
+ )
72
+ assistant: Optional[PromptSectionConfig] = Field(
73
+ default=None,
74
+ description="Assistant response configuration (target for training)"
75
+ )
76
+
77
+ # Custom sections for flexibility
78
+ custom_sections: Dict[str, PromptSectionConfig] = Field(
79
+ default_factory=dict,
80
+ description="Additional custom sections"
81
+ )
82
+
83
+ # Section ordering
84
+ section_order: List[str] = Field(
85
+ default=["system", "context", "user", "reasoning", "assistant"],
86
+ description="Order of sections in the prompt"
87
+ )
88
+
89
+ # Special tokens
90
+ bos_token: Optional[str] = Field(default=None, description="Beginning of sequence token")
91
+ eos_token: Optional[str] = Field(default=None, description="End of sequence token")
92
+ pad_token: Optional[str] = Field(default=None, description="Padding token")
93
+
94
+ # Separator configuration
95
+ section_separator: str = Field(default="\n\n", description="Separator between sections")
96
+ message_separator: str = Field(default="\n", description="Separator between messages")
97
+
98
+ # Instruction format (for instruction-tuned models)
99
+ instruction_format: str = Field(
100
+ default="none",
101
+ description="Preset format: none, alpaca, chatml, llama3, mistral, vicuna, phi3"
102
+ )
103
+
104
+ def get_template_for_format(self, format_name: str) -> Dict[str, Any]:
105
+ """Get preset template configuration for known formats"""
106
+ presets = {
107
+ "none": {},
108
+ "alpaca": {
109
+ "system": PromptSectionConfig(
110
+ enabled=True,
111
+ template="Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.",
112
+ prefix="", suffix="\n\n"
113
+ ),
114
+ "user": PromptSectionConfig(
115
+ enabled=True,
116
+ template="### Instruction:\n{instruction}\n\n### Input:\n{input}",
117
+ columns=["instruction", "input"],
118
+ prefix="", suffix="\n\n"
119
+ ),
120
+ "assistant": PromptSectionConfig(
121
+ enabled=True,
122
+ template="### Response:\n{output}",
123
+ columns=["output"],
124
+ prefix="", suffix=""
125
+ ),
126
+ "section_order": ["system", "user", "assistant"]
127
+ },
128
+ "chatml": {
129
+ "system": PromptSectionConfig(
130
+ enabled=True,
131
+ template="{system_message}",
132
+ columns=["system_message"],
133
+ prefix="<|im_start|>system\n", suffix="<|im_end|>\n"
134
+ ),
135
+ "user": PromptSectionConfig(
136
+ enabled=True,
137
+ template="{user_message}",
138
+ columns=["user_message"],
139
+ prefix="<|im_start|>user\n", suffix="<|im_end|>\n"
140
+ ),
141
+ "assistant": PromptSectionConfig(
142
+ enabled=True,
143
+ template="{assistant_message}",
144
+ columns=["assistant_message", "output", "response"],
145
+ prefix="<|im_start|>assistant\n", suffix="<|im_end|>"
146
+ ),
147
+ "section_order": ["system", "user", "assistant"]
148
+ },
149
+ "llama3": {
150
+ "system": PromptSectionConfig(
151
+ enabled=True,
152
+ template="{system_message}",
153
+ columns=["system_message"],
154
+ prefix="<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n",
155
+ suffix="<|eot_id|>"
156
+ ),
157
+ "user": PromptSectionConfig(
158
+ enabled=True,
159
+ template="{user_message}",
160
+ columns=["user_message", "question"],
161
+ prefix="<|start_header_id|>user<|end_header_id|>\n\n",
162
+ suffix="<|eot_id|>"
163
+ ),
164
+ "assistant": PromptSectionConfig(
165
+ enabled=True,
166
+ template="{assistant_message}",
167
+ columns=["assistant_message", "output", "response"],
168
+ prefix="<|start_header_id|>assistant<|end_header_id|>\n\n",
169
+ suffix="<|eot_id|>"
170
+ ),
171
+ "section_order": ["system", "user", "assistant"]
172
+ },
173
+ "mistral": {
174
+ "system": PromptSectionConfig(
175
+ enabled=True,
176
+ template="{system_message}",
177
+ columns=["system_message"],
178
+ prefix="[INST] ", suffix=" "
179
+ ),
180
+ "user": PromptSectionConfig(
181
+ enabled=True,
182
+ template="{user_message}",
183
+ columns=["user_message", "question"],
184
+ prefix="", suffix=" [/INST]"
185
+ ),
186
+ "assistant": PromptSectionConfig(
187
+ enabled=True,
188
+ template="{assistant_message}",
189
+ columns=["assistant_message", "output"],
190
+ prefix="", suffix="</s>"
191
+ ),
192
+ "section_order": ["system", "user", "assistant"]
193
+ },
194
+ "vicuna": {
195
+ "system": PromptSectionConfig(
196
+ enabled=True,
197
+ template="A chat between a curious user and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the user's questions.",
198
+ prefix="", suffix=" "
199
+ ),
200
+ "user": PromptSectionConfig(
201
+ enabled=True,
202
+ template="{user_message}",
203
+ columns=["user_message", "question"],
204
+ prefix="USER: ", suffix=" "
205
+ ),
206
+ "assistant": PromptSectionConfig(
207
+ enabled=True,
208
+ template="{assistant_message}",
209
+ columns=["assistant_message", "output"],
210
+ prefix="ASSISTANT: ", suffix=""
211
+ ),
212
+ "section_order": ["system", "user", "assistant"]
213
+ },
214
+ "phi3": {
215
+ "system": PromptSectionConfig(
216
+ enabled=True,
217
+ template="{system_message}",
218
+ columns=["system_message"],
219
+ prefix="<|system|>\n", suffix="<|end|>\n"
220
+ ),
221
+ "user": PromptSectionConfig(
222
+ enabled=True,
223
+ template="{user_message}",
224
+ columns=["user_message", "question"],
225
+ prefix="<|user|>\n", suffix="<|end|>\n"
226
+ ),
227
+ "assistant": PromptSectionConfig(
228
+ enabled=True,
229
+ template="{assistant_message}",
230
+ columns=["assistant_message", "output"],
231
+ prefix="<|assistant|>\n", suffix="<|end|>"
232
+ ),
233
+ "section_order": ["system", "user", "assistant"]
234
+ },
235
+ "reasoning": {
236
+ "system": PromptSectionConfig(
237
+ enabled=True,
238
+ template="You are a helpful AI assistant that thinks step by step before responding.",
239
+ prefix="", suffix="\n\n"
240
+ ),
241
+ "context": PromptSectionConfig(
242
+ enabled=True,
243
+ template="{context}",
244
+ columns=["context", "passages", "background"],
245
+ prefix="Context:\n", suffix="\n\n"
246
+ ),
247
+ "user": PromptSectionConfig(
248
+ enabled=True,
249
+ template="{question}",
250
+ columns=["question", "query", "user_message"],
251
+ prefix="Question: ", suffix="\n\n"
252
+ ),
253
+ "reasoning": PromptSectionConfig(
254
+ enabled=True,
255
+ template="{reasoning}",
256
+ columns=["reasoning", "thinking", "chain_of_thought"],
257
+ prefix="Reasoning:\n", suffix="\n\n"
258
+ ),
259
+ "assistant": PromptSectionConfig(
260
+ enabled=True,
261
+ template="{answer}",
262
+ columns=["answer", "output", "response"],
263
+ prefix="Answer: ", suffix=""
264
+ ),
265
+ "section_order": ["system", "context", "user", "reasoning", "assistant"]
266
+ }
267
+ }
268
+ return presets.get(format_name, {})
269
+
270
+
271
+ # ============================================
272
+ # COLUMN MAPPING CONFIGURATION
273
+ # ============================================
274
+
275
+ class ColumnMappingConfig(BaseModel):
276
+ """Maps dataset columns to training roles"""
277
+ # Primary text columns
278
+ text_column: Optional[str] = Field(None, description="Main text column (for causal LM)")
279
+ input_column: Optional[str] = Field(None, description="Input text column")
280
+ output_column: Optional[str] = Field(None, description="Output/target column")
281
+
282
+ # Multi-column support
283
+ instruction_column: Optional[str] = Field(None, description="Instruction column")
284
+ question_column: Optional[str] = Field(None, description="Question column")
285
+ answer_column: Optional[str] = Field(None, description="Answer column")
286
+ context_column: Optional[str] = Field(None, description="Context/passages column")
287
+ reasoning_column: Optional[str] = Field(None, description="Reasoning/CoT column")
288
+
289
+ # Classification specific
290
+ label_column: Optional[str] = Field(None, description="Label column for classification")
291
+ label_mapping: Optional[Dict[str, int]] = Field(None, description="Label to ID mapping")
292
+
293
+ # NER/Token classification
294
+ tokens_column: Optional[str] = Field(None, description="Tokens column (for NER)")
295
+ tags_column: Optional[str] = Field(None, description="NER tags column")
296
+ ner_tags_mapping: Optional[Dict[str, str]] = Field(None, description="NER tag to label mapping")
297
+
298
+ # QA specific
299
+ title_column: Optional[str] = Field(None, description="Title column for QA context")
300
+ id_column: Optional[str] = Field(None, description="ID column")
301
+ answers_column: Optional[str] = Field(None, description="Answers column (list of answers)")
302
+ start_position_column: Optional[str] = Field(None, description="Start position column")
303
+ end_position_column: Optional[str] = Field(None, description="End position column")
304
+
305
+ # Additional context
306
+ metadata_columns: List[str] = Field(
307
+ default_factory=list,
308
+ description="Additional columns to include as metadata"
309
+ )
310
+
311
+ # Column transformations
312
+ column_transforms: Dict[str, str] = Field(
313
+ default_factory=dict,
314
+ description="Transformations to apply: column -> transform_type"
315
+ )
316
+
317
+ # Custom column aliases
318
+ column_aliases: Dict[str, str] = Field(
319
+ default_factory=dict,
320
+ description="Map custom column names to standard names"
321
+ )
322
+
323
+ def get_effective_column(self, role: str) -> Optional[str]:
324
+ """Get the effective column name for a given role, considering aliases"""
325
+ role_to_column = {
326
+ "text": self.text_column,
327
+ "input": self.input_column,
328
+ "output": self.output_column,
329
+ "instruction": self.instruction_column,
330
+ "question": self.question_column,
331
+ "answer": self.answer_column,
332
+ "context": self.context_column,
333
+ "reasoning": self.reasoning_column,
334
+ "label": self.label_column,
335
+ "tokens": self.tokens_column,
336
+ "tags": self.tags_column,
337
+ "title": self.title_column,
338
+ "id": self.id_column,
339
+ "answers": self.answers_column,
340
+ }
341
+ column = role_to_column.get(role)
342
+ if column and column in self.column_aliases:
343
+ return self.column_aliases[column]
344
+ return column
345
+
346
+
347
+ # ============================================
348
+ # DATASET CONFIGURATION (ENHANCED)
349
+ # ============================================
350
+
351
+ class DatasetSplitConfig(BaseModel):
352
+ """Configuration for a single dataset split"""
353
+ name: str = Field(default="train", description="Split name (train, validation, test)")
354
+ enabled: bool = Field(default=True)
355
+ max_samples: Optional[int] = Field(None, description="Maximum samples to use")
356
+ shuffle: bool = Field(default=True)
357
+ seed: int = Field(default=42)
358
+ stratify: bool = Field(default=False, description="Stratified sampling")
359
+
360
+
361
  class DatasetConfig(BaseModel):
362
+ """Enhanced dataset configuration with full control"""
363
+ # Source configuration
364
+ source: str = Field(default="huggingface", description="Dataset source: huggingface, local, upload")
365
  name: Optional[str] = Field(None, description="HuggingFace dataset name")
366
+ config: Optional[str] = Field(None, description="Dataset config/subset name")
367
+ revision: Optional[str] = Field(None, description="Dataset revision/branch")
368
+
369
+ # Split configuration
370
+ splits: List[DatasetSplitConfig] = Field(
371
+ default_factory=lambda: [DatasetSplitConfig(name="train")],
372
+ description="Dataset splits to use"
373
+ )
374
+ train_split: str = Field(default="train", description="Training split name")
375
+ validation_split: Optional[str] = Field(None, description="Validation split name")
376
+ test_split: Optional[str] = Field(None, description="Test split name")
377
+
378
+ # Split generation
379
+ validation_split_ratio: float = Field(
380
+ default=0.1,
381
+ ge=0.0,
382
+ le=0.5,
383
+ description="Ratio for auto-generating validation split"
384
+ )
385
+ generate_validation: bool = Field(
386
+ default=True,
387
+ description="Auto-generate validation split if not provided"
388
+ )
389
+
390
+ # Column mapping
391
+ column_mapping: ColumnMappingConfig = Field(
392
+ default_factory=ColumnMappingConfig,
393
+ description="Map dataset columns to training roles"
394
+ )
395
+
396
+ # Prompt template
397
+ prompt_template: Optional[PromptTemplateConfig] = Field(
398
+ default=None,
399
+ description="Prompt structure configuration"
400
+ )
401
+
402
+ # Data processing
403
  max_length: int = Field(default=512, description="Max sequence length")
404
  max_target_length: int = Field(default=128, description="Max target length for seq2seq")
405
+ truncate: bool = Field(default=True, description="Truncate sequences exceeding max_length")
406
+
407
+ # Filtering
408
+ filter_conditions: List[Dict[str, Any]] = Field(
409
+ default_factory=list,
410
+ description="Filter conditions: [{column, operator, value}]"
411
+ )
412
+ min_text_length: Optional[int] = Field(None, description="Minimum text length")
413
+ max_text_length: Optional[int] = Field(None, description="Maximum text length")
414
+
415
+ # Augmentation
416
+ augmentation_enabled: bool = Field(default=False)
417
+ augmentation_config: Dict[str, Any] = Field(
418
+ default_factory=dict,
419
+ description="Data augmentation configuration"
420
+ )
421
+
422
+ # Streaming (for large datasets)
423
+ streaming: bool = Field(
424
+ default=False,
425
+ description="Use streaming mode for large datasets"
426
+ )
427
+ streaming_buffer_size: int = Field(default=10000)
428
+
429
+ # Caching
430
+ cache_dir: Optional[str] = Field(None, description="Cache directory")
431
+ num_proc: int = Field(default=4, description="Number of processes for data loading")
432
+
433
+ # Local file support
434
  path: Optional[str] = Field(None, description="Local file path")
435
+ file_type: Optional[str] = Field(None, description="File type: json, jsonl, csv, parquet, text")
436
+
437
+ # Data validation
438
+ validate_data: bool = Field(default=True, description="Validate data before training")
439
+ validation_sample_size: int = Field(default=100, description="Samples to validate")
440
 
441
 
442
+ # ============================================
443
+ # TRAINING ARGUMENTS CONFIGURATION
444
+ # ============================================
445
+
446
  class TrainingArgsConfig(BaseModel):
447
  """Training arguments configuration."""
448
  epochs: int = Field(default=3, ge=1, le=100)
 
462
  bf16: bool = Field(default=False)
463
  gradient_checkpointing: bool = Field(default=False)
464
  optimizer: str = Field(default="adamw_torch")
465
+ lr_scheduler_type: str = Field(default="cosine")
466
  report_to: str = Field(default="none")
467
  seed: int = Field(default=42)
468
+
469
+ # Advanced options
470
+ eval_strategy: str = Field(default="steps")
471
+ load_best_model_at_end: bool = Field(default=True)
472
+ metric_for_best_model: str = Field(default="eval_loss")
473
+ greater_is_better: bool = Field(default=False)
474
+
475
+ # Memory optimization
476
+ optim: str = Field(default="adamw_torch")
477
+ ddp_find_unused_parameters: bool = Field(default=False)
478
+
479
 
480
 
481
  class PEFTConfig(BaseModel):
482
  """PEFT/LoRA configuration."""
483
  enabled: bool = Field(default=True)
484
+ method: str = Field(default="lora", description="lora, adalora, ia3, prefix_tuning, prompt_tuning")
485
  r: int = Field(default=16, ge=1, le=256, description="LoRA rank")
486
  alpha: int = Field(default=32, ge=1, description="LoRA alpha")
487
  dropout: float = Field(default=0.05, ge=0.0, le=0.5)
488
  target_modules: List[str] = Field(default=["q_proj", "v_proj"])
489
  bias: str = Field(default="none")
490
+ modules_to_save: List[str] = Field(default_factory=list)
491
+
492
+ # AdaLoRA specific
493
+ init_r: int = Field(default=12)
494
+ t_init: int = Field(default=200)
495
+ t_final: int = Field(default=1000)
496
+
497
+ # Prefix Tuning specific
498
+ num_virtual_tokens: int = Field(default=20)
499
+
500
+ # Prompt Tuning specific
501
+ num_tokens: int = Field(default=20)
502
+ token_init: bool = Field(default=True)
503
 
504
 
505
  class OutputConfig(BaseModel):
 
507
  push_to_hub: bool = Field(default=False)
508
  hub_model_id: Optional[str] = Field(None)
509
  private: bool = Field(default=False)
510
+ save_strategy: str = Field(default="steps")
511
+ output_dir: Optional[str] = Field(None)
512
 
513
 
514
  class TrainingRequest(BaseModel):
515
+ """Full training request with all configuration options."""
516
  name: str = Field(..., min_length=1, max_length=255)
517
  description: Optional[str] = Field(None)
518
  task_type: str = Field(default="causal-lm")
 
530
  "causal-lm", "seq2seq", "token-classification",
531
  "sequence-classification", "question-answering",
532
  "summarization", "translation", "text-classification",
533
+ "masked-lm", "vision-classification", "audio-classification",
534
+ "reasoning"
535
  ]
536
  if v not in valid_types:
537
  raise ValueError(f"Invalid task_type. Must be one of: {valid_types}")
 
567
  hub_model_id: Optional[str]
568
 
569
 
570
+ class DatasetPreviewResponse(BaseModel):
571
+ """Dataset preview with detected columns and sample data."""
572
+ dataset_name: str
573
+ config: Optional[str]
574
+ splits: List[str]
575
+ columns: List[Dict[str, Any]]
576
+ sample_data: List[Dict[str, Any]]
577
+ total_rows: Optional[int]
578
+ detected_task_types: List[str]
579
+ suggested_column_mapping: ColumnMappingConfig
580
+
581
+
582
  # Global queue instance
583
  training_queue = None
584
 
 
591
  return training_queue
592
 
593
 
594
+ # ============================================
595
+ # DATASET PREVIEW AND COLUMN DETECTION
596
+ # ============================================
597
+
598
+ @router.get("/dataset/preview/{dataset_name:path}", response_model=DatasetPreviewResponse)
599
+ async def preview_dataset(
600
+ dataset_name: str,
601
+ config: Optional[str] = None,
602
+ split: str = "train",
603
+ rows: int = 10
604
+ ):
605
+ """Preview a dataset with detected columns and suggested mappings."""
606
+ try:
607
+ from datasets import load_dataset
608
+
609
+ # Load dataset
610
+ if config:
611
+ ds = load_dataset(dataset_name, config, split=split, trust_remote_code=True, streaming=False)
612
+ else:
613
+ ds = load_dataset(dataset_name, split=split, trust_remote_code=True, streaming=False)
614
+
615
+ # Get column info
616
+ columns = []
617
+ for col_name, col_type in ds.features.items():
618
+ col_info = {
619
+ "name": col_name,
620
+ "type": str(col_type),
621
+ "dtype": type(col_type).__name__,
622
+ }
623
+ # Detect if it's a label column
624
+ if hasattr(col_type, 'names'):
625
+ col_info["labels"] = col_type.names
626
+ col_info["num_labels"] = len(col_type.names)
627
+ columns.append(col_info)
628
+
629
+ # Get sample data
630
+ sample_data = []
631
+ for i in range(min(rows, len(ds))):
632
+ sample_data.append(ds[i])
633
+
634
+ # Detect task type and suggest column mapping
635
+ detected_tasks, suggested_mapping = detect_task_and_mapping(ds, columns)
636
+
637
+ return DatasetPreviewResponse(
638
+ dataset_name=dataset_name,
639
+ config=config,
640
+ splits=list(ds.info.splits.keys()) if hasattr(ds, 'info') and ds.info.splits else [split],
641
+ columns=columns,
642
+ sample_data=sample_data,
643
+ total_rows=len(ds),
644
+ detected_task_types=detected_tasks,
645
+ suggested_column_mapping=suggested_mapping
646
+ )
647
+
648
+ except Exception as e:
649
+ raise HTTPException(status_code=400, detail=f"Error loading dataset: {str(e)}")
650
+
651
+
652
+ def detect_task_and_mapping(dataset, columns: List[Dict]) -> tuple:
653
+ """Detect suitable task types and suggest column mappings."""
654
+ col_names = [c["name"].lower() for c in columns]
655
+ col_names_original = [c["name"] for c in columns]
656
+ detected_tasks = []
657
+ mapping = ColumnMappingConfig()
658
+
659
+ # Check for text classification
660
+ label_cols = [c for c in columns if "label" in c["name"].lower() or c.get("labels")]
661
+ text_cols = [c for c in columns if "text" in c["name"].lower()]
662
+
663
+ if label_cols and text_cols:
664
+ detected_tasks.append("text-classification")
665
+ mapping.text_column = text_cols[0]["name"]
666
+ mapping.label_column = label_cols[0]["name"]
667
+ if label_cols[0].get("labels"):
668
+ mapping.label_mapping = {name: i for i, name in enumerate(label_cols[0]["labels"])}
669
+
670
+ # Check for QA
671
+ question_cols = [c for c in columns if "question" in c["name"].lower()]
672
+ answer_cols = [c for c in columns if "answer" in c["name"].lower()]
673
+ context_cols = [c for c in columns if "context" in c["name"].lower()]
674
+
675
+ if question_cols and answer_cols:
676
+ detected_tasks.append("question-answering")
677
+ mapping.question_column = question_cols[0]["name"]
678
+ mapping.answer_column = answer_cols[0]["name"]
679
+ if context_cols:
680
+ mapping.context_column = context_cols[0]["name"]
681
+
682
+ # Check for instruction/output
683
+ instruction_cols = [c for c in columns if "instruction" in c["name"].lower() or "prompt" in c["name"].lower()]
684
+ output_cols = [c for c in columns if "output" in c["name"].lower() or "response" in c["name"].lower()]
685
+
686
+ if instruction_cols and output_cols:
687
+ detected_tasks.append("causal-lm")
688
+ mapping.instruction_column = instruction_cols[0]["name"]
689
+ mapping.output_column = output_cols[0]["name"]
690
+
691
+ # Check for input/target (seq2seq)
692
+ input_cols = [c for c in columns if "input" in c["name"].lower() or "source" in c["name"].lower()]
693
+ target_cols = [c for c in columns if "target" in c["name"].lower() or "summary" in c["name"].lower()]
694
+
695
+ if input_cols and target_cols:
696
+ detected_tasks.append("seq2seq")
697
+ mapping.input_column = input_cols[0]["name"]
698
+ mapping.output_column = target_cols[0]["name"]
699
+
700
+ # Check for NER
701
+ tokens_cols = [c for c in columns if "token" in c["name"].lower() or "word" in c["name"].lower()]
702
+ tags_cols = [c for c in columns if "tag" in c["name"].lower() or "ner" in c["name"].lower()]
703
+
704
+ if tokens_cols and tags_cols:
705
+ detected_tasks.append("token-classification")
706
+ mapping.tokens_column = tokens_cols[0]["name"]
707
+ mapping.tags_column = tags_cols[0]["name"]
708
+
709
+ # Default to causal LM if we have any text
710
+ if not detected_tasks:
711
+ text_like = [c for c in columns if c["dtype"] in ["Value", "LargeString"]]
712
+ if text_like:
713
+ detected_tasks.append("causal-lm")
714
+ mapping.text_column = text_like[0]["name"]
715
+
716
+ return detected_tasks, mapping
717
+
718
+
719
+ # ============================================
720
+ # PROMPT TEMPLATE ENDPOINTS
721
+ # ============================================
722
+
723
+ @router.get("/prompt-templates")
724
+ async def get_prompt_templates():
725
+ """Get available prompt template presets."""
726
+ return {
727
+ "presets": [
728
+ {
729
+ "id": "none",
730
+ "name": "None (Raw Text)",
731
+ "description": "Use dataset text directly without formatting"
732
+ },
733
+ {
734
+ "id": "alpaca",
735
+ "name": "Alpaca Format",
736
+ "description": "Instruction-Input-Output format for instruction tuning"
737
+ },
738
+ {
739
+ "id": "chatml",
740
+ "name": "ChatML",
741
+ "description": "ChatML format used by various models"
742
+ },
743
+ {
744
+ "id": "llama3",
745
+ "name": "Llama 3",
746
+ "description": "Llama 3 instruction format"
747
+ },
748
+ {
749
+ "id": "mistral",
750
+ "name": "Mistral",
751
+ "description": "Mistral/Vicuna instruction format"
752
+ },
753
+ {
754
+ "id": "vicuna",
755
+ "name": "Vicuna",
756
+ "description": "Vicuna chat format"
757
+ },
758
+ {
759
+ "id": "phi3",
760
+ "name": "Phi-3",
761
+ "description": "Microsoft Phi-3 format"
762
+ },
763
+ {
764
+ "id": "reasoning",
765
+ "name": "Reasoning/CoT",
766
+ "description": "Chain-of-thought reasoning format with explicit thinking"
767
+ }
768
+ ]
769
+ }
770
+
771
+
772
+ @router.get("/prompt-templates/{template_id}")
773
+ async def get_prompt_template(template_id: str):
774
+ """Get specific prompt template configuration."""
775
+ config = PromptTemplateConfig(instruction_format=template_id)
776
+ preset = config.get_template_for_format(template_id)
777
+
778
+ if not preset and template_id != "none":
779
+ raise HTTPException(status_code=404, detail="Template not found")
780
+
781
+ return {
782
+ "id": template_id,
783
+ "config": preset
784
+ }
785
+
786
+
787
+ # ============================================
788
+ # TRAINING JOB ENDPOINTS
789
+ # ============================================
790
+
791
  @router.post("/start", response_model=TrainingJobResponse)
792
  async def start_training(
793
  request: TrainingRequest,
 
808
  dataset_source=request.dataset.source,
809
  dataset_name=request.dataset.name,
810
  dataset_config=request.dataset.config,
811
+ dataset_split=request.dataset.train_split,
812
  training_args=request.training_args.dict(),
813
  peft_config=request.peft_config.dict() if request.peft_config else None,
814
  status=JobStatus.PENDING.value,
 
819
  db.add(training_job)
820
  await db.commit()
821
 
822
+ # Build full config
823
  config = {
824
  "job_id": job_id,
825
  "task_type": request.task_type,
 
854
  db: AsyncSession = Depends(get_db)
855
  ):
856
  """Get status of a training job."""
 
857
  queue = get_queue()
858
  queue_status = await queue.get_status(job_id)
859
 
 
860
  result = await db.execute(
861
  select(TrainingJob).where(TrainingJob.job_id == job_id)
862
  )
 
865
  if not job:
866
  raise HTTPException(status_code=404, detail="Job not found")
867
 
 
868
  if queue_status:
869
  job.status = queue_status.get("status", job.status)
870
  if queue_status.get("progress"):
 
945
  if not cancelled:
946
  raise HTTPException(status_code=400, detail="Cannot cancel job")
947
 
 
948
  result = await db.execute(
949
  select(TrainingJob).where(TrainingJob.job_id == job_id)
950
  )