File size: 12,431 Bytes
b96f3a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
"""

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
        }