File size: 15,655 Bytes
6e5a6d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import pandas as pd
import numpy as np
from rank_bm25 import BM25Okapi
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
from spellchecker import SpellChecker
import re
import os
import logging
from transformers import DistilBertTokenizer, DistilBertForSequenceClassification, Trainer, TrainingArguments
from torch.utils.data import Dataset
import torch
from sklearn.preprocessing import LabelEncoder

# Setup logging
logging.basicConfig(filename='chatbot.log', level=logging.INFO, format='%(asctime)s - %(message)s')

# Configuration
INTENTS = [
    "visa_application", "other", "travel", "job", "education", "culture",
    "integration", "healthcare", "market", "housing", "citizenship"
]
SYNONYMS = {
    "st paddy's day": "st patrick's day",
    "lepp card": "leap card",
    "resdence": "residence",
    "job": "employment",
    "study permit": "student visa",
    "irish stuff": "irish culture",
    "gnib": "irp card",
    "contact": "reach",
    "INIS": "immigration service",
    "pub toasts": "pub culture",
    "homesick": "homesickness",
    "travel": "transport"
}
IRISH_TERMS = {
    "fáilte": "welcome",
    "sláinte": "cheers",
    "céilí": "dance",
    "bealtaine": "May festival",
    "lá fhéile pádraig": "St. Patrick’s Day",
    "craic": "fun",
    "bodhrán": "drum",
    "gaeilge": "Irish language"
}
MAPPINGS = {
    "how to leap card?": ("how do i get a leap card in ireland?", "travel"),
    "what is contact of INIS?": ("how do i contact inis?", "visa_application"),
    "what’s the contact for INIS office?": ("how do i contact inis?", "visa_application"),
    "what’s the deal with st paddy’s day?": ("what is st patricks day and how can i enjoy it?", "culture"),
    "how do i live in Ireland?": ("how do i apply for permanent residency in ireland?", "visa_application"),
    "tell me about irish stuff.": ("what are irish music and dance traditions?", "culture"),
    "i am feeling home sick in ireland": ("how do i handle homesickness in dublin?", "integration"),
    "how can i get a work visa if i’m from brazil?": ("how do i apply for a work permit from brazil?", "visa_application"),
    "how to explore ireland as a tourist on a budget?": ("what are budget travel tips for tourists?", "travel"),
    "hello, do you know about GIP": ("what is the purpose of the start-up entrepreneur programme (step)?", "visa_application"),
    "tell me more about those pub toasts!": ("how do i learn about irish pub culture?", "culture"),
    "what’s the process for a student visa in Ireland?": ("what are the steps to apply for a study visa in ireland?", "visa_application"),
    "how to travel in dublin?": ("how do i travel around dublin?", "travel"),
    "i’m homesick in dublin, any tips?": ("how do i handle homesickness in dublin?", "integration"),
    "good night": ("none", "other"),
    "bye": ("none", "other"),
    "i am so good": ("none", "other")
}

# Preprocessing
spell = SpellChecker()

def normalize_query(query):
    query = query.lower().strip()
    query = re.sub(r'[^\w\s?]', '', query)
    return query

def correct_typos(query):
    words = query.split()
    corrected = [spell.correction(word) if spell.correction(word) else word for word in words]
    return " ".join(corrected)

def expand_synonyms(query):
    for synonym, canonical in SYNONYMS.items():
        query = query.replace(synonym, canonical)
    return query

def clean_irish_terms(response):
    response = response.replace("Antwort: ", "").replace("answer: ", "").strip(": ")
    for term, meaning in IRISH_TERMS.items():
        response = re.sub(rf"\*{re.escape(term)}\*\s*\([^)]*\)", f"*{term}*", response)
        response = re.sub(rf"\*{re.escape(term)}\*", f"*{term}* ({meaning})", response)
    return response

def preprocess_query(query):
    query = normalize_query(query)
    query = correct_typos(query)
    query = expand_synonyms(query)
    return query

# Intent Classification
class IntentDataset(Dataset):
    def __init__(self, questions, intents, tokenizer, max_length=128):
        self.questions = questions
        self.intents = intents
        self.tokenizer = tokenizer
        self.max_length = max_length

    def __len__(self):
        return len(self.questions)

    def __getitem__(self, idx):
        question = self.questions[idx]
        intent = self.intents[idx]
        encoding = self.tokenizer(
            question,
            max_length=self.max_length,
            padding='max_length',
            truncation=True,
            return_tensors='pt'
        )
        return {
            'input_ids': encoding['input_ids'].squeeze(),
            'attention_mask': encoding['attention_mask'].squeeze(),
            'labels': torch.tensor(intent, dtype=torch.long)
        }

class IntentClassifier:
    def __init__(self):
        self.tokenizer = DistilBertTokenizer.from_pretrained('distilbert-base-uncased')
        self.model = DistilBertForSequenceClassification.from_pretrained('distilbert-base-uncased', num_labels=len(INTENTS))
        self.label_encoder = LabelEncoder()
        self.model_path = './distilbert_finetuned'

    def fine_tune(self, dataset):
        intents = self.label_encoder.fit_transform(dataset["intent"])
        questions = dataset["question"].tolist()
        df = pd.DataFrame({"question": questions, "intent": intents})
        balanced_df = []
        for intent in set(intents):
            intent_df = df[df["intent"] == intent]
            if len(intent_df) < 20:
                intent_df = pd.concat([intent_df] * (20 // len(intent_df) + 1), ignore_index=True)[:20]
            balanced_df.append(intent_df)
        balanced_df = pd.concat(balanced_df)
        train_dataset = IntentDataset(balanced_df["question"].tolist(), balanced_df["intent"].tolist(), self.tokenizer)
        training_args = TrainingArguments(
            output_dir=self.model_path,
            num_train_epochs=4,
            per_device_train_batch_size=8,
            warmup_steps=500,
            weight_decay=0.01,
            logging_dir='./logs',
            logging_steps=10,
            save_strategy="epoch"
        )
        trainer = Trainer(
            model=self.model,
            args=training_args,
            train_dataset=train_dataset
        )
        trainer.train()
        self.model.save_pretrained(self.model_path)
        self.tokenizer.save_pretrained(self.model_path)

    def predict(self, query):
        if os.path.exists(self.model_path):
            self.model = DistilBertForSequenceClassification.from_pretrained(self.model_path)
        inputs = self.tokenizer(query, return_tensors="pt", truncation=True, padding=True)
        outputs = self.model(**inputs)
        predicted_class = torch.argmax(outputs.logits, dim=1).item()
        return self.label_encoder.inverse_transform([predicted_class])[0]

# Retrieval
class Retriever:
    def __init__(self, dataset):
        self.dataset = dataset
        self.model = SentenceTransformer('all-MiniLM-L6-v2')
        self.tokenized_corpus = [q.lower().split() for q in dataset["question"]]
        self.bm25 = BM25Okapi(self.tokenized_corpus)
        self.question_embeddings = self.model.encode(dataset["question"].tolist())

    def check_mapping(self, query):
        if query.lower() in MAPPINGS:
            mapped_q, intent = MAPPINGS[query.lower()]
            return mapped_q, 0.9, intent
        return None, 0.0, None

    def handle_vague_query(self, query):
        if any(keyword in query.lower() for keyword in ["stuff", "things", "general"]) or \
           ("culture" in query.lower() and "irish" in query.lower()):
            culture_questions = self.dataset[self.dataset["intent"] == "culture"]
            if culture_questions.empty:
                return None, 0.0, None
            culture_indices = culture_questions.index.tolist()
            scores = self.bm25.get_scores(query.lower().split())
            culture_scores = scores[culture_indices]
            if len(culture_scores) == 0:
                return None, 0.0, None
            top_idx = culture_scores.argmax()
            return culture_questions["question"].iloc[top_idx], 0.9, "culture"
        return None, 0.0, None

    def combine_scores(self, bm25_scores, sent_scores, bm25_weight=0.4, sent_weight=0.6):
        bm25_norm = bm25_scores / (bm25_scores.max() + 1e-10)
        sent_norm = sent_scores / (sent_scores.max() + 1e-10)
        return bm25_weight * bm25_norm + sent_weight * sent_norm

    def retrieve(self, query, threshold=0.5):
        query = preprocess_query(query)

        # Check mappings
        mapped_q, score, intent = self.check_mapping(query)
        if mapped_q:
            return mapped_q, score, intent

        # Check vague queries
        vague_q, vague_score, vague_intent = self.handle_vague_query(query)
        if vague_q:
            return vague_q, vague_score, vague_intent

        # BM25 + SentenceTransformer
        tokenized_query = query.lower().split()
        bm25_scores = self.bm25.get_scores(tokenized_query)
        query_embedding = self.model.encode([query])
        sent_scores = cosine_similarity(query_embedding, self.question_embeddings)[0]
        combined_scores = self.combine_scores(bm25_scores, sent_scores)

        top_idx = combined_scores.argmax()
        similarity = combined_scores[top_idx]

        if similarity >= threshold:
            return self.dataset["question"].iloc[top_idx], similarity, self.dataset["intent"].iloc[top_idx]
        return None, 0.0, None

# Response Generation
class ResponseGenerator:
    def __init__(self):
        self.tokenizer = None
        self.model = None

    def generate(self, query, retrieved_q, dataset, intent, similarity):
        if not retrieved_q or retrieved_q == "none":
            if query.lower() in ["good night", "bye"]:
                return "Cheers, catch you later! *Sláinte* (cheers)!", []
            if query.lower() == "i am so good":
                return "Glad you’re feeling the *craic* (fun)! What’s up next for your Irish adventure?", []
            return f"Sorry, I couldn’t find a match! Try asking about visas, jobs, or Irish culture for some *craic* (fun) answers!", []
        answer = dataset[dataset["question"] == retrieved_q]["answer"].iloc[0]
        prefix = "Yo, here’s the *craic* (fun) on that: " if intent in ["culture", "travel"] else ""
        response = clean_irish_terms(prefix + answer.strip())
        return response, [(retrieved_q, similarity, intent)]

# Main Function
def main():
    # Load and clean dataset
    dataset = pd.read_csv("dataset.csv")
    dataset["question"] = dataset["question"].str.lower().str.replace(r"[^\w\s?]", "", regex=True)
    dataset["answer"] = dataset["answer"].str.replace(r"\*{2}([^*]+)\*{2}", r"*\1*", regex=True)
    dataset["intent"] = dataset["intent"].str.lower().replace("greeting", "other")
    print("Dataset Intent Distribution:")
    print(dataset["intent"].value_counts())

    # Initialize components
    retriever = Retriever(dataset)
    classifier = IntentClassifier()
    generator = ResponseGenerator()

    # Fine-tune classifier
    classifier.fine_tune(dataset)

    # Test queries
    test_queries = [
        "how to leap card?",
        "What’s the deal with St. Paddy’s Day?",
        "Tell me about Irish stuff.",
        "I am EU citizen do I need visa to work in Ireland?",
        "what is contact of INIS?",
        "how to get irp card?",
        "What is saint patrick’s day?",
        "I’m from Nigeria, how do I study in Ireland?",
        "hi",
        "Hi, How do I apply for a Work Permit?",
        "Tell me about artificial intelligence.",
        "What’s the weather like?",
        "how are you?",
        "Hello, I need help with my application.",
        "Hello, do I need work permit to work in Ireland?",
        "Hi, what is the process for getting a visa?",
        "Hello, How do I apply for a Study Permit in Ireland?",
        "Hi, Can I retire in Ireland?",
        "Hello, do you know about GIP",
        "good morning",
        "thanks for your answer",
        "I am from india do I need visa to work in Ireland?",
        "Good night",
        "Bye",
        "Thank you for your help!",
        "I am so good",
        "Can you tell some irish culture?",
        "what is farmer's market?",
        "How to attend a Gaelic football match?",
        "what is irp card",
        "how to get irish citizenship?",
        "how to get irish passport?",
        "how to travel in dublin?",
        "how to get a job in ireland?",
        "how apply for irish residency?",
        "How can I get a work visa if I’m from Brazil?",
        "What’s the process for a student visa in Ireland?",
        "I’m an EU citizen, do I need a permit to work in Dublin?",
        "How to obtain a residence card in Ireland?",
        "What’s a GNIB card?",
        "Where can I find cheap accommodation in Galway?",
        "How to rent an apartment in Ireland as a non-EU student?",
        "What’s the cost of living in Cork for a worker?",
        "What’s a *céilí* dance and how do I join one?",
        "How can I celebrate Bealtaine in Ireland?",
        "Tell me about Irish music with a *bodhrán*.",
        "Hey, what’s that festival with parades in March?",
        "Tell me more about those pub toasts!",
        "How to get an irish resdence card?",
        "What’s the contact for INIS office?",
        "Can I retire in Cork as a US citizen?",
        "I’m Australian, how do I get a job in Ireland?",
        "What’s the process for getting a PPS number?",
        "I’m homesick in Dublin, any tips?",
        "How to join Irish community groups as an immigrant?",
        "What’s a permit?",
        "How do I live in Ireland?"
    ]

    results = []
    for query in test_queries:
        logging.info(f"Processing query: {query}")
        preprocessed_query = preprocess_query(query)
        logging.info(f"Preprocessed query: {preprocessed_query}")

        retrieved_q, similarity, intent = retriever.retrieve(query)
        logging.info(f"Retrieved: {retrieved_q}, Similarity: {similarity}, Intent: {intent}")

        if not retrieved_q:
            intent = classifier.predict(preprocessed_query)
            logging.info(f"Fallback intent: {intent}")

        response, top_matches = generator.generate(query, retrieved_q, dataset, intent, similarity)
        logging.info(f"Response: {response}")

        result = {
            "Query": query,
            "Retrieved Question": retrieved_q,
            "Similarity": similarity,
            "Predicted Intent": intent,
            "Response": response,
            "Top Matches": top_matches
        }
        results.append(result)
        print(f"Query: {result['Query']}")
        print(f"Debug: Query={result['Query']}, Similarity={result['Similarity']:.3f}, Predicted Intent={result['Predicted Intent']}, Retrieved Q={result['Retrieved Question']}")
        print(f"Response: {result['Response']}")
        print(f"Top Matches: {result['Top Matches']}")
        print()

    # Save results
    with open("chatbot_results.txt", "w") as f:
        for result in results:
            f.write(f"Query: {result['Query']}\n")
            f.write(f"Debug: Query={result['Query']}, Similarity={result['Similarity']:.3f}, Predicted Intent={result['Predicted Intent']}, Retrieved Q={result['Retrieved Question']}\n")
            f.write(f"Response: {result['Response']}\n")
            f.write(f"Top Matches: {result['Top Matches']}\n\n")

if __name__ == "__main__":
    main()