File size: 26,418 Bytes
6e7d23c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c041586
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6e7d23c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1270a97
 
6e7d23c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9cbd80
6e7d23c
 
 
 
 
5fc8a03
b9cbd80
 
 
 
 
 
 
 
 
 
6e7d23c
 
b9cbd80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0bb414f
b9cbd80
 
6e7d23c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9cbd80
 
 
 
 
 
 
 
 
 
 
 
 
 
6e7d23c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9cbd80
 
6e7d23c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9cbd80
6e7d23c
 
 
 
 
 
 
 
 
 
 
 
5fc8a03
6e7d23c
 
 
5fc8a03
6e7d23c
 
 
5fc8a03
6e7d23c
 
 
5fc8a03
6e7d23c
 
 
5fc8a03
6e7d23c
 
 
b9cbd80
6e7d23c
 
 
b9cbd80
6e7d23c
 
 
 
 
 
 
 
 
 
 
 
b9cbd80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6e7d23c
 
 
 
 
 
 
 
 
 
 
 
 
 
0f0370c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6e7d23c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
import json
import os
import random
import shutil

# ==========================================
# 1. PATH CONFIGURATIONS
# ==========================================
# Paths to input source files and output generated datasets

# Path where the raw blogs/products JSON file is stored
RAW_DATA_PATH = "data/mintoak/mintoak_data.json"

# Path where the training set JSONL will be saved (used for fine-tuning)
TRAIN_OUT_PATH = "data/mintoak/train.jsonl"

# Path where the validation set JSONL will be saved (used to evaluate fine-tuning)
VALID_OUT_PATH = "data/mintoak/valid.jsonl"

# Path where the clean chunked passages are saved (used by RAG vector database)
RAG_OUT_PATH = "data/mintoak/mintoak_chunks.json"


# ==========================================
# 2. DATA PROCESSING FUNCTIONS
# ==========================================

def load_raw_data(path):
    """
    Loads and normalizes raw website content.
    Supports both standard simplified format (title, url, content) 
    and complex nested Strapi CMS export structures.
    """
    if not os.path.exists(path):
        print(f"Error: Raw data file {path} not found.")
        print("Please place your Mintoak products/blogs JSON file at that path.")
        return None
        
    with open(path, 'r', encoding='utf-8') as f:
        data = json.load(f)
        
    # Check if the data is wrapped in a Strapi CMS 'data' dict or is a direct list
    if isinstance(data, dict) and "data" in data:
        raw_items = data["data"]
    elif isinstance(data, list):
        raw_items = data
    else:
        raw_items = []
        
    normalized_items = []
    for item in raw_items:
        if not isinstance(item, dict):
            continue
            
        # Format 1: If it's already in the Simplified Format, load it directly
        if "url" in item or "content" in item:
            normalized_items.append({
                "url": item.get("url", "https://www.mintoak.com"),
                "title": item.get("title", "Mintoak Page"),
                "content": item.get("content", ""),
                "category": item.get("category", "general")
            })
            continue
            
        # Format 2: Parse nested Strapi CRM exporter format
        attributes = item.get("attributes") or {}
        
        # Format 2A: Product format
        product_content_list = attributes.get("product") or []
        if product_content_list:
            slug = attributes.get("slug") or ""
            url = f"https://www.mintoak.com/products/{slug}" if slug else "https://www.mintoak.com"
            title = attributes.get("breadcrumbTitle")
            content_parts = []
            category = "Product offering"
            
            for p_item in product_content_list:
                if not isinstance(p_item, dict):
                    continue
                # Extract description or listing text
                desc = p_item.get("description") or p_item.get("ListingShortDescription") or p_item.get("metaDescription")
                if desc:
                    content_parts.append(desc)
                # Try to extract title if not set yet
                if not title and p_item.get("__component") == "shared.seo":
                    title = p_item.get("metaTitle")
            
            if not title:
                title = "Mintoak Product"
                
            content = "\n\n".join(content_parts)
            
            normalized_items.append({
                "url": url.strip(),
                "title": title.strip(),
                "content": content.strip(),
                "category": category
            })
            continue

        # Format 2C: About Us / Visionaries format
        about_us_list = attributes.get("aboutUs") or []
        if about_us_list:
            url = "https://www.mintoak.com/about-us"
            title = None
            for au_item in about_us_list:
                if not isinstance(au_item, dict):
                    continue
                sec_titles = au_item.get("sectionTitle") or []
                for st in sec_titles:
                    if isinstance(st, dict) and st.get("title"):
                        title = st.get("title")
                        break
                if title:
                    break
            if not title:
                title = "Mintoak Founders"
                
            content_parts = []
            for au_item in about_us_list:
                if not isinstance(au_item, dict):
                    continue
                if au_item.get("__component") == "about-us.mintoak-visionaries":
                    team_members = au_item.get("teamMembers") or []
                    members_descriptions = []
                    for member in team_members:
                        name = member.get("title", "").strip()
                        role = member.get("subTitle", "").strip()
                        desc = member.get("description", "").strip()
                        members_descriptions.append(f"{name} ({role} - {desc})")
                    
                    content_parts.append("Mintoak founders: " + ", ".join(members_descriptions) + ".")
            
            content = " ".join(content_parts)
            category = "Company Info"
            
            normalized_items.append({
                "url": url.strip(),
                "title": title.strip(),
                "content": content.strip(),
                "category": category
            })
            continue

        # Format 2B: Blog format
        slug = attributes.get("slug") or ""
        url = f"https://www.mintoak.com/blog/{slug}" if slug else "https://www.mintoak.com"
        
        title = None
        # Extract title from components list
        top_title_desc = attributes.get("TopTittleDescription") or []
        for title_desc in top_title_desc:
            if isinstance(title_desc, dict) and title_desc.get("__component") == "reference.title":
                title = title_desc.get("title")
                break
        if not title:
            # Fallback to SEO meta title if description title is missing
            seo = attributes.get("seo") or {}
            title = seo.get("metaTitle", "Mintoak Page")
            
        # Get category label safely
        category_obj = attributes.get("blog_master_category") or {}
        category_data = category_obj.get("data") or {}
        category_attr = category_data.get("attributes") or {}
        category = category_attr.get("category", "general")
        
        # Merge all paragraphs under BlogContent into a single block of text
        content_parts = []
        blog_content = attributes.get("BlogContent") or []
        for content_item in blog_content:
            if isinstance(content_item, dict):
                description = content_item.get("description")
                if description:
                    content_parts.append(description)
        content = "\n\n".join(content_parts)
        
        normalized_items.append({
            "url": url.strip(),
            "title": title.strip() if title else "Mintoak Page",
            "content": content.strip(),
            "category": category.strip() if category else "general"
        })
        
    return normalized_items


def chunk_content(text, max_chars=800):
    """
    Splits long articles into smaller passages (chunks) of roughly max_chars length.
    This ensures that when a passage is retrieved, it fits within the model's memory, 
    and keeps sentences intact so sentences don't get chopped in half.
    """
    sentences = text.split(". ")
    chunks = []
    current_chunk = []
    current_len = 0
    for s in sentences:
        s_len = len(s)
        # If adding this sentence exceeds chunk size limit, save current chunk and start a new one
        if current_len + s_len > max_chars:
            if current_chunk:
                chunks.append(". ".join(current_chunk) + ".")
            current_chunk = [s]
            current_len = s_len
        else:
            current_chunk.append(s)
            current_len += s_len + 2
    if current_chunk:
        chunks.append(". ".join(current_chunk) + ".")
    return chunks


def build_rag_chunks(raw_data):
    """
    Iterates through all normalized articles and splits them into chunks with metadata.
    """
    chunks_db = []
    for idx, item in enumerate(raw_data):
        url = item.get("url", "https://www.mintoak.com")
        title = item.get("title", "Mintoak Page")
        content = item.get("content", "")
        category = item.get("category", "general")
        
        # Split content into smaller passages
        chunk_size = 1200 if category == "Company Info" else 800
        passages = chunk_content(content, max_chars=chunk_size)
        for p_idx, passage in enumerate(passages):
            chunks_db.append({
                "id": f"doc_{idx}_chunk_{p_idx}",
                "url": url,
                "title": title,
                "content": passage.strip(),
                "category": category
            })
    return chunks_db


def generate_synthetic_dialogue(chunks_db):
    """
    Generates synthetic training pairs to teach the model:
    1. Grounding: Answer strictly based on the provided context.
    2. Citations: Reference the exact URL/section.
    3. Fallback: Refuse to answer if not in the context.
    4. General/Greetings: Correctly answer greetings and identity questions.
    5. Lead Capture: Model conversational ask-then-tag flow.
    """
    qa_pairs = []
    
    # Prompt template for fine-tuning system instructions
    system_prompt = (
        "You are the Mintoak Website Assistant, a senior, business-aware digital advisor on the Mintoak website. "
        "Your tone is professional, clear, confident, technology-forward, and benefit-led. Do not use hype, casualness, or over-enthusiasm. "
        "Do not use emojis by default; use at most one emoji only where it adds clarity (e.g. 💡, 📈) and never in pricing, credibility, or trust contexts. "
        "Answer strictly using the provided context. Never invent, assume, or pull from outside knowledge. "
        "Start your response directly and naturally—do NOT use robotic preambles like 'As per the context' or 'Based on the provided context'. "
        "Frame Mintoak as a white-labeled, modular, cloud-native, API-first SaaS platform/solution provider (never as a 'software company', 'software', 'tool', or 'app'). "
        "Avoid these banned phrases: 'seamless', 'empowering', 'leverage' (as a verb), 'game-changer', 'synergy', 'unlocking', 'in today\'s fast-paced world', 'as we move forward'. "
        "Follow this exact structure: Context (direct answer) -> Insight (translate features into merchant/business outcomes like activation, cost-to-acquire, time-to-market) -> Direction (nudge to the next best page/demo and close with the source URL). "
        "Keep responses highly concise (maximum 2-3 sentences or short bullets) unless explicitly asked for detail. "
        "Output tail order: answer -> source URL(s) -> [CAPTURE_LEAD] (if applicable). "
        "If the context does not contain the answer, reply exactly: 'The requested information does not currently exist on www.mintoak.com. You can get in touch with our team at https://www.mintoak.com/contact-us.'"
    )

    def clean_banned_words(text):
        import re
        # Case-insensitive replacement of banned words
        text = re.sub(r'\bseamlessly\b', 'efficiently', text, flags=re.I)
        text = re.sub(r'\bseamless\b', 'integrated', text, flags=re.I)
        text = re.sub(r'\bempowers\b', 'enables', text, flags=re.I)
        text = re.sub(r'\bempowering\b', 'enabling', text, flags=re.I)
        text = re.sub(r'\bempower\b', 'enable', text, flags=re.I)
        text = re.sub(r'\bleveraged\b', 'utilized', text, flags=re.I)
        text = re.sub(r'\bleverages\b', 'utilizes', text, flags=re.I)
        text = re.sub(r'\bleveraging\b', 'utilizing', text, flags=re.I)
        text = re.sub(r'\bleverage\b', 'use', text, flags=re.I)
        text = re.sub(r'\bgame-changer\b', 'significant advancement', text, flags=re.I)
        text = re.sub(r'\bgame changer\b', 'significant advancement', text, flags=re.I)
        text = re.sub(r'\bsynergy\b', 'alignment', text, flags=re.I)
        text = re.sub(r'\bsynergies\b', 'alignments', text, flags=re.I)
        text = re.sub(r'\bunlocking\b', 'releasing', text, flags=re.I)
        text = re.sub(r'\bunlock\b', 'access', text, flags=re.I)
        text = re.sub(r"\bin today's fast-paced world,?\s*", "", text, flags=re.I)
        text = re.sub(r"\bas we move forward,?\s*", "", text, flags=re.I)
        
        text = re.sub(r'\bMintoak (\w+ )?(?:software|tool|app)\b', r'Mintoak \1platform', text, flags=re.I)
        text = re.sub(r'\b(?:software|tool|app) Mintoak\b', 'Mintoak platform', text, flags=re.I)
        
        # Force Mintoak capitalization
        text = re.sub(r'(?<![\./])\b(MintOak|MINTOAK|mintoak|Mentoak|mentoak|Mintoack|mintoack)\b(?![\./])', 'Mintoak', text)
        return text

    # 1. Generate Positive Examples (grounded QA)
    for chunk in chunks_db:
        context_str = f"Source: {chunk['url']}\nContent: {chunk['content']}"
        
        # Filter rules to target content matching Mintoak or merchant topics
        title_lower = chunk['title'].lower()
        content_lower = chunk['content'].lower()
        category_lower = chunk['category'].lower()
        
        if "360" in title_lower or "merchant" in content_lower or "product" in category_lower or "qr" in title_lower:
            questions = [
                f"What is {chunk['title']}?",
                f"Can you explain {chunk['title']} and its benefits?",
                f"How does {chunk['title']} help merchants?",
                f"Tell me about {chunk['title']}."
            ]
            q = random.choice(questions)
            
            # Extract first 2 sentences
            sentences = [s.strip() for s in chunk['content'].split('.') if s.strip()]
            summary_sentences = sentences[:2]
            summary_text = ". ".join(summary_sentences)
            if summary_text and not summary_text.endswith('.'):
                summary_text += '.'
                
            # Structured Response: Context -> Insight -> Direction
            context_ans = clean_banned_words(summary_text)
            
            insight_choices = [
                "This enables banks to deep-dive into merchant needs and support business growth.",
                "This module helps acquirers improve merchant retention and increase transaction metrics.",
                "By optimizing these services, banks can reduce cost-to-acquire and accelerate merchant time-to-market.",
                "This serves as a key driver for business growth, helping merchants scale their daily operations."
            ]
            insight_text = random.choice(insight_choices)
            
            direction_text = f"👉 For more details, visit: [{chunk['title']}]({chunk['url']})"
            
            a = f"{context_ans}\n\n{insight_text}\n\n{direction_text}"
            
            qa_pairs.append({
                "messages": [
                    {"role": "system", "content": system_prompt},
                    {"role": "user", "content": f"Context:\n{context_str}\n\nQuestion: {q}"},
                    {"role": "assistant", "content": a}
                ]
            })
            
        # 1B. Generate Catalog List Examples (for list/summary queries)
        if chunk['id'] == "synthetic_master_catalog":
            catalog_questions = [
                "give me list of mintoak product offerings",
                "give me list of mintoak product offerings and explain each one liner",
                "what are the product offerings of mintoak",
                "list all products of mintoak and their descriptions",
                "give me all product names and one liners",
                "what products does mintoak offer"
            ]
            for cq in catalog_questions:
                catalog_cleaned = clean_banned_words(chunk['content'])
                ca = f"{catalog_cleaned}\n\n👉 For more details, visit: [{chunk['title']}]({chunk['url']})"
                qa_pairs.append({
                    "messages": [
                        {"role": "system", "content": system_prompt},
                        {"role": "user", "content": f"Context:\n{context_str}\n\nQuestion: {cq}"},
                        {"role": "assistant", "content": ca}
                    ]
                })

    # 2. Generate Negative/Fallback Examples (out-of-domain QA)
    out_of_domain_questions = [
        "What is the weather in Mumbai today?",
        "How do I sign up for a Netflix account?",
        "Tell me about the payments page or FAQs.",
        "What is the capital of France?",
        "Can I get a refund on my purchase?"
    ]
    
    fallback_response = (
        "The requested information does not currently exist on www.mintoak.com. "
        "You can get in touch with our team at https://www.mintoak.com/contact-us."
    )

    for q in out_of_domain_questions:
        unrelated_context = "Source: https://www.mintoak.com\nContent: Mintoak is a SaaS platform partnering with banks to support merchants."
        qa_pairs.append({
            "messages": [
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": f"Context:\n{unrelated_context}\n\nQuestion: {q}"},
                {"role": "assistant", "content": fallback_response}
            ]
        })

    # 3. Generate General/Greeting/Identity Examples (positive handling)
    general_examples = [
        {
            "q": "who are you?",
            "a": "I am the Mintoak Website Assistant, a digital advisor here to help you understand our SaaS platform and merchant solutions."
        },
        {
            "q": "who are u?",
            "a": "I am the Mintoak Website Assistant. How can I help you today?"
        },
        {
            "q": "what is your name?",
            "a": "I am the Mintoak Website Assistant."
        },
        {
            "q": "hi",
            "a": "Hello! I am the Mintoak Website Assistant. How can I help you today?"
        },
        {
            "q": "hello",
            "a": "Hello! I am the Mintoak Website Assistant. How can I help you today?"
        },
        {
            "q": "what is mintoak?",
            "a": "Mintoak is a white-labeled, modular, cloud-native SaaS platform that partners with banks and acquirers to enable and support merchants. It provides integrated modules for digital payment acceptance, reconciliation, settlements, and business analytics."
        },
        {
            "q": "what is mintoak's offering?",
            "a": "Mintoak offers a white-labeled, modular SaaS platform for banks and acquirers. Key modules include digital payment acceptance, automated transaction reconciliation, settlement tracking, and data-driven analytics to support merchants."
        }
    ]

    for item in general_examples:
        qa_pairs.append({
            "messages": [
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": f"Context:\n\nQuestion: {item['q']}"},
                {"role": "assistant", "content": item['a']}
            ]
        })

    # 4. Generate Conversational Lead Capture Examples
    lead_capture_examples = [
        {
            "history": [
                {"role": "user", "content": "I would like to schedule a demo of Mintoak DigiOnboard"}
            ],
            "a": "We would be glad to show you how DigiOnboard supports merchant acquisition. Could you please share your name and email address so our business team can set up a live demonstration?"
        },
        {
            "history": [
                {"role": "user", "content": "How much does Mintoak SmartPayments cost?"}
            ],
            "a": "Pricing for SmartPayments is customized based on your transaction volume and deployment needs. Could you please provide your name and email address so we can share a detailed proposal?"
        },
        {
            "history": [
                {"role": "user", "content": "How much does Engage360 cost?"},
                {"role": "assistant", "content": "Pricing for Engage360 is tailored to each acquirer's deployment. Could you please share your name and email address so we can provide a customized quote?"},
                {"role": "user", "content": "Sure, my name is Alice Smith and my email is alice@bank.com"}
            ],
            "a": "Thank you, Alice. A Mintoak representative will reach out to you shortly at alice@bank.com with our custom pricing structure. For more details, visit: [Mintoak Engage360](https://www.mintoak.com/products/mintoak-engage360) [CAPTURE_LEAD]"
        },
        {
            "history": [
                {"role": "user", "content": "Can you contact me about partnering? My name is Bob and email is bob@acquirer.com"}
            ],
            "a": "Thank you for your interest in partnering with Mintoak, Bob. Our partnerships team will contact you shortly at bob@acquirer.com to discuss collaboration opportunities. You can learn more about us at: [About Us](https://www.mintoak.com/about-us) [CAPTURE_LEAD]"
        }
    ]

    for item in lead_capture_examples:
        messages = [{"role": "system", "content": system_prompt}]
        for hist_item in item["history"]:
            messages.append({"role": hist_item["role"], "content": hist_item["content"]})
        qa_pairs.append({
            "messages": messages + [{"role": "assistant", "content": item["a"]}]
        })

    return qa_pairs


# ==========================================
# 3. MAIN RUNNER
# ==========================================

def main():
    # Load and clean the raw data
    raw_data = load_raw_data(RAW_DATA_PATH)
    if not raw_data:
        return
        
    print(f"Loaded {len(raw_data)} website items.")

    # Dynamically extract and combine all office addresses under contact-us
    office_contents = []
    other_items = []
    contact_us_item = None
    
    for item in raw_data:
        url = item.get("url", "")
        if url.startswith("https://www.mintoak.com/contact-us#"):
            office_contents.append(item.get("content", "").strip())
        elif url == "https://www.mintoak.com/contact-us":
            contact_us_item = item
        else:
            other_items.append(item)
            
    if office_contents:
        combined_address_content = "Mintoak office locations and addresses:\n\n" + "\n\n".join(f"* {content}" for content in office_contents)
        if contact_us_item:
            contact_us_item["content"] = combined_address_content
            contact_us_item["title"] = "Mintoak Office Addresses"
        else:
            contact_us_item = {
                "url": "https://www.mintoak.com/contact-us",
                "title": "Mintoak Office Addresses",
                "content": combined_address_content,
                "category": "Company Info"
            }
            
    if contact_us_item:
        other_items.append(contact_us_item)
        
    raw_data = other_items
    
    # Process text into chunks
    chunks_db = build_rag_chunks(raw_data)
    
    # Programmatically compile Master Catalog Chunk with one-liner descriptions (deduplicated)
    product_dict = {}
    for item in raw_data:
        if item.get('category') == 'Product offering':
            p_title = item.get('title', '').strip()
            p_content = item.get('content', '').strip()
            
            # Extract the first sentence or first line as a clean one-liner
            first_sentence = p_content.split('\n')[0].split('. ')[0].strip()
            if first_sentence and not first_sentence.endswith('.'):
                first_sentence += '.'
                
            # Limit description length for the catalog summary chunk
            if len(first_sentence) > 160:
                first_sentence = first_sentence[:157] + "..."
                
            if p_title and first_sentence and p_title not in product_dict:
                product_dict[p_title] = first_sentence
                
    product_details = [f"* {k}: {v}" for k, v in sorted(product_dict.items())]
                
    if product_details:
        catalog_content = "Mintoak offers the following product offerings and descriptions:\n" + "\n".join(product_details)
        chunks_db.append({
            "id": "synthetic_master_catalog",
            "url": "https://www.mintoak.com/products",
            "title": "Mintoak Product List Catalog",
            "content": catalog_content,
            "category": "Product offering"
        })
        print(f"Generated synthetic master catalog chunk with {len(product_details)} unique product one-liners.")
        
    print(f"Generated {len(chunks_db)} RAG chunks.")
    
    # Save the chunks to the RAG database source file
    with open(RAG_OUT_PATH, 'w', encoding='utf-8') as f:
        json.dump(chunks_db, f, indent=4)
    print(f"Saved RAG index database to {RAG_OUT_PATH}")
    
    # Clear Vector DB cache to force rebuild on next assistant run
    shutil.rmtree("data/mintoak/chroma_db", ignore_errors=True)
    print("Cleared Vector DB cache directory to force index rebuild.")
    
    # Generate dialogue pairs for training/validation
    qa_pairs = generate_synthetic_dialogue(chunks_db)
    print(f"Generated {len(qa_pairs)} training/validation pairs.")
    
    # Shuffle and split into 80% train and 20% validation sets
    random.shuffle(qa_pairs)
    split_idx = int(len(qa_pairs) * 0.8)
    train_data = qa_pairs[:split_idx]
    valid_data = qa_pairs[split_idx:]
    
    # Save datasets in JSONL (JSON Lines) format required by MLX fine-tuning
    with open(TRAIN_OUT_PATH, 'w', encoding='utf-8') as f:
        for item in train_data:
            f.write(json.dumps(item) + "\n")
            
    with open(VALID_OUT_PATH, 'w', encoding='utf-8') as f:
        for item in valid_data:
            f.write(json.dumps(item) + "\n")
            
    print(f"Saved {len(train_data)} training examples to {TRAIN_OUT_PATH}")
    print(f"Saved {len(valid_data)} validation examples to {VALID_OUT_PATH}")

if __name__ == "__main__":
    main()