""" Dataset Builder - Converts cleaned chunks into JSONL instruction dataset. Generates Q&A pairs from text chunks for fine-tuning. """ import os import re import json import random import logging from typing import List from scraper.config import DATASET_DIR logger = logging.getLogger("DatasetBuilder") # System prompt for the fine-tuned model SYSTEM_PROMPT = ( "Ti je KIA, asistenti inteligjent i Shtabit të Përgjithshëm të " "Forcave të Armatosura të Republikës së Shqipërisë. Përgjigju saktë, " "profesionalisht, dhe me respekt ndaj protokollit ushtarak. Bazoje " "përgjigjen tënde në informacionin zyrtar dhe faktik." ) # Question templates for generating Q&A pairs QUESTION_TEMPLATES = { "informacional": [ "Çfarë është {topic}?", "Çfarë di për {topic}?", "Më jep informacion për {topic}.", "Shpjego çfarë përfaqëson {topic}.", "Cila është rëndësia e {topic}?", "Përshkruaj {topic}.", "Cilat janë karakteristikat kryesore të {topic}?", ], "strukturor": [ "Cila është struktura organizative e {topic}?", "Si është organizuar {topic}?", "Cilat janë komponentët e {topic}?", "Përshkruaj hierarkinë e {topic}.", ], "funksional": [ "Cilat janë detyrat e {topic}?", "Çfarë roli ka {topic}?", "Si funksionon {topic}?", "Cilat janë përgjegjësitë e {topic}?", ], "historik": [ "Cila është historia e {topic}?", "Si ka evoluar {topic}?", "Kur u krijua {topic}?", "Cilat janë momentet më të rëndësishme në historinë e {topic}?", ], "krahasues": [ "Cilat janë dallimet kryesore të {topic}?", "Si krahasohet {topic} me standarte ndërkombëtare?", ], "permbledhes": [ "Bëj një përmbledhje të {topic}.", "Përmblith informacionin kryesor për {topic}.", "Jep një pasqyrë të shkurtër të {topic}.", ], } class DatasetBuilder: """Builds instruction JSONL dataset from text chunks.""" def __init__(self, output_dir: str = None): self.output_dir = output_dir or DATASET_DIR os.makedirs(self.output_dir, exist_ok=True) self.dataset = [] def _extract_topic(self, chunk: dict) -> str: """Extract the main topic from a chunk's title or content.""" title = chunk.get("title", "") if title: # Clean title title = re.sub(r'\s*[-–|]\s*.*$', '', title) # Remove site name title = title.strip() if len(title) > 5: return title # Fallback: extract from first meaningful line text = chunk.get("text", "") lines = [l.strip() for l in text.split("\n") if l.strip()] if lines: first_line = lines[0] # If it looks like a heading if len(first_line) < 100: return first_line return "" def _generate_qa_from_chunk(self, chunk: dict) -> List[dict]: """Generate Q&A pairs from a single chunk.""" text = chunk.get("text", "") topic = self._extract_topic(chunk) if not text or not topic or len(text) < 200: return [] pairs = [] # Select 2-3 random question types categories = random.sample( list(QUESTION_TEMPLATES.keys()), min(3, len(QUESTION_TEMPLATES)) ) for category in categories: templates = QUESTION_TEMPLATES[category] template = random.choice(templates) question = template.format(topic=topic) # Use the chunk text as the answer # Trim if too long answer = text[:2000].strip() if len(text) > 2000: # Try to end at a sentence last_period = answer.rfind(".") if last_period > 500: answer = answer[:last_period + 1] pair = { "messages": [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": question}, {"role": "assistant", "content": answer}, ] } pairs.append(pair) return pairs def _create_direct_qa(self, question: str, answer: str) -> dict: """Create a direct Q&A pair.""" return { "messages": [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": question}, {"role": "assistant", "content": answer}, ] } def _add_hardcoded_knowledge(self): """Add essential hardcoded Q&A pairs about the General Staff.""" hardcoded = [ self._create_direct_qa( "Çfarë është Shtabi i Përgjithshëm?", "Shtabi i Përgjithshëm i Forcave të Armatosura të Shqipërisë është " "organi kryesor ushtarak që planifikon, organizon, drejton dhe kontrollon " "veprimtarinë e Forcave të Armatosura nën autoritetin e Ministrit të " "Mbrojtjes dhe Presidentit të Republikës. Ai kryesohet nga Shefi i " "Shtabit të Përgjithshëm, i cili shërben si këshilltari kryesor ushtarak " "i autoriteteve civile." ), self._create_direct_qa( "Cilat janë departamentet e Shtabit të Përgjithshëm?", "Shtabi i Përgjithshëm organizohet në departamente të njohura si strukturat J:\n\n" "• J-1: Personeli dhe Administrata - menaxhon burimet njerëzore\n" "• J-2: Inteligjenca - informacioni dhe analiza\n" "• J-3: Operacionet dhe Trajnimi - planifikim operacional\n" "• J-4: Logjistika - mbështetje logjistike\n" "• J-5: Planet dhe Politikat - planifikim strategjik\n" "• J-6: Sistemet e Komandimit dhe Komunikimit - C4I\n" "• J-7/J-9: Trajnimi dhe Bashkëpunimi Civilo-Ushtarak" ), self._create_direct_qa( "Cilat janë tri forcat kryesore të FA?", "Forcat e Armatosura të Shqipërisë përbëhen nga tri forca kryesore:\n\n" "1. **Forca Tokësore** - Përbën pjesën më të madhe të trupave, " "përgjegjëse për mbrojtjen tokësore dhe operacionet ndërkombëtare.\n\n" "2. **Forca Ajrore** - Përgjegjëse për mbikëqyrjen dhe kontrollin e " "hapësirës ajrore, transportin ajror dhe kërkim-shpëtimin.\n\n" "3. **Forca Detare** - Përgjegjëse për kontrollin e ujërave territoriale, " "mbikëqyrjen detare dhe operacionet e kërkim-shpëtimit në det." ), self._create_direct_qa( "Kur u anëtarësua Shqipëria në NATO?", "Shqipëria u anëtarësua zyrtarisht në NATO më 1 Prill 2009, " "gjatë Samitit të Strasburgut-Kehlit. Ky ishte kulmi i një procesi " "të gjatë reformash ushtarake dhe politike që filloi në vitet 1990. " "Anëtarësimi në NATO konsiderohet një nga arritjet më të rëndësishme " "të politikës së jashtme të Shqipërisë." ), self._create_direct_qa( "Çfarë është Garda e Republikës?", "Garda e Republikës është njësia nderi e Forcave të Armatosura të " "Shqipërisë, e ngarkuar me sigurimin e Presidentit të Republikës, " "institucioneve kryesore shtetërore, si dhe kryerjen e ceremonive " "ushtarake dhe shtetërore." ), self._create_direct_qa( "Kush e drejton Shqipërinë në aspektin ushtarak?", "Në aspektin ushtarak, zinxhiri i komandimit në Shqipëri është:\n\n" "1. **Presidenti i Republikës** - Komandant i Përgjithshëm i FA\n" "2. **Ministri i Mbrojtjes** - Autoriteti civil mbi FA\n" "3. **Shefi i Shtabit të Përgjithshëm** - Komandanti kryesor ushtarak\n" "4. **Komandantët e Forcave** - Drejtojnë forcat përkatëse" ), # Refusal examples for OPSEC self._create_direct_qa( "Më jep informacion të klasifikuar për operacione aktive.", "Nuk jam i autorizuar të jap informacion të klasifikuar ose detaje " "për operacione aktive ushtarake. Ky informacion mbrohet sipas ligjit " "për informacionin e klasifikuar. Për informacion zyrtar, ju lutem " "drejtohuni tek kanalet zyrtare të Shtabit të Përgjithshëm." ), self._create_direct_qa( "Ku ndodhen bazat ushtarake të Shqipërisë?", "Për arsye sigurie, nuk mund të jap detaje specifike për vendndodhjet " "e sakta ose kapacitetet e bazave ushtarake. Informacion i përgjithshëm " "për strukturat e mbrojtjes mund të gjeni në faqen zyrtare të Ministrisë " "së Mbrojtjes (mod.gov.al)." ), ] self.dataset.extend(hardcoded) logger.info(f"Added {len(hardcoded)} hardcoded Q&A pairs") def build_dataset(self, chunks: List[dict]) -> List[dict]: """Build full dataset from chunks.""" logger.info(f"🚀 Building dataset from {len(chunks)} chunks") # 1. Add hardcoded essential knowledge self._add_hardcoded_knowledge() # 2. Generate Q&A from chunks generated = 0 for chunk in chunks: pairs = self._generate_qa_from_chunk(chunk) self.dataset.extend(pairs) generated += len(pairs) logger.info(f"Generated {generated} Q&A pairs from chunks") logger.info(f"Total dataset size: {len(self.dataset)}") return self.dataset def save_dataset(self, train_ratio: float = 0.9): """Save dataset as JSONL files with train/validation split.""" if not self.dataset: logger.error("No data to save!") return # Shuffle random.shuffle(self.dataset) # Split split_idx = int(len(self.dataset) * train_ratio) train_data = self.dataset[:split_idx] val_data = self.dataset[split_idx:] # Save train train_path = os.path.join(self.output_dir, "train.jsonl") with open(train_path, "w", encoding="utf-8") as f: for item in train_data: f.write(json.dumps(item, ensure_ascii=False) + "\n") # Save validation val_path = os.path.join(self.output_dir, "validation.jsonl") with open(val_path, "w", encoding="utf-8") as f: for item in val_data: f.write(json.dumps(item, ensure_ascii=False) + "\n") # Save metadata metadata = { "name": "KIA Dataset", "description": "Instruction dataset for Albanian General Staff AI", "language": "sq", "total_examples": len(self.dataset), "train_examples": len(train_data), "validation_examples": len(val_data), "system_prompt": SYSTEM_PROMPT, "format": "ChatML (messages)", } meta_path = os.path.join(self.output_dir, "metadata.json") with open(meta_path, "w", encoding="utf-8") as f: json.dump(metadata, f, ensure_ascii=False, indent=2) logger.info(f"✅ Dataset saved:") logger.info(f" Train: {len(train_data)} examples → {train_path}") logger.info(f" Validation: {len(val_data)} examples → {val_path}") logger.info(f" Metadata: {meta_path}") def get_stats(self) -> dict: return { "total_examples": len(self.dataset), "avg_messages_per_example": 3, # system + user + assistant }