File size: 16,619 Bytes
001bd50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import os
import gradio as gr
import logging
import asyncio
from dotenv import load_dotenv
from langchain.prompts import PromptTemplate
from langchain_qdrant import QdrantVectorStore
from langchain.chains import RetrievalQA
from langchain_groq import ChatGroq
from qdrant_client.models import PointStruct, VectorParams, Distance
import uuid
from qdrant_client.http import models
from datetime import datetime
from langchain_community.embeddings.fastembed import FastEmbedEmbeddings
from qdrant_client import QdrantClient
import cohere
from langchain.retrievers import ContextualCompressionRetriever
from langchain_cohere import CohereRerank
import re
from translation_service import TranslationService

# Load environment variables
load_dotenv()

# Initialize logging with INFO level and detailed format
logging.basicConfig(
    filename='app.log',
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

# Initialize services
translator = TranslationService()

def initialize_database_client():
    """Initialize Qdrant client"""
    try:
        client = QdrantClient(
            url=os.getenv("QDURL"),
            api_key=os.getenv("API_KEY1"),
            verify=True # Set to True if using SSL
        )
        logging.info("Qdrant client initialized successfully.")
        return client
    except Exception as e:
        logging.error(f"Failed to initialize Qdrant client: {e}")
        raise

def initialize_llm():
    """Initialize LLM with fallback"""
    try:
        llm = ChatGroq(
            temperature=0, 
            model_name="llama3-8b-8192", 
            api_key=os.getenv("GROQ_API_KEY")
        )
        logging.info("ChatGroq initialized with model llama3-8b-8192.")
        return llm
    except Exception as e:
        logging.warning(f"Failed to initialize ChatGroq with llama3: {e}. Falling back to mixtral.")
        try:
            llm = ChatGroq(
                temperature=0, 
                model_name="mixtral-8x7b-32768", 
                api_key=os.getenv("GROQ_API_KEY")
            )
            logging.info("ChatGroq initialized with fallback model mixtral-8x7b-32768.")
            return llm
        except Exception as fallback_e:
            logging.error(f"Failed to initialize fallback LLM: {fallback_e}")
            raise

def initialize_services():
    """Initialize all services"""
    try:
        # Initialize Qdrant client
        client = initialize_database_client()
        
        # Initialize embeddings
        embeddings = FastEmbedEmbeddings(model_name="nomic-ai/nomic-embed-text-v1.5-Q")
        logging.info("FastEmbedEmbeddings initialized successfully.")
        
        # Initialize Qdrant DB
        db = QdrantVectorStore(
            client=client,
            embedding=embeddings,
            collection_name="RR3"
        )
        logging.info("QdrantVectorStore initialized with collection 'RR3'.")
        
        # Initialize retriever with reranker
        cohere_client = cohere.Client(api_key=os.getenv("COHERE_API_KEY"))
        reranker = CohereRerank(
            client=cohere_client,
            top_n=3,
            model="rerank-multilingual-v3.0"
        )
        base_retriever = db.as_retriever(search_kwargs={"k": 14})
        retriever = ContextualCompressionRetriever(
            base_compressor=reranker, 
            base_retriever=base_retriever
        )
        logging.info("Retriever with reranker initialized successfully.")
        
        # Initialize LLM
        llm = initialize_llm()
        
        return retriever, llm
    except Exception as e:
        logging.error(f"Service initialization error: {str(e)}")
        raise

def initialize_feedback_collection():
    """Initialize and verify feedback collection"""
    try:
        client = initialize_database_client()
        
        # Check if collection exists
        collections = client.get_collections().collections
        collection_exists = any(c.name == "chat_feedback" for c in collections)
        
        if not collection_exists:
            # Create collection with proper configuration
            client.create_collection(
                collection_name="chat_feedback",
                vectors_config=VectorParams(
                    size=768,  # Ensure this matches the embedding size
                    distance=Distance.COSINE
                )
            )
            logging.info("Created 'chat_feedback' collection with vector size 768 and Cosine distance.")
        else:
            logging.info("'chat_feedback' collection already exists.")
        
        # Verify collection exists and has correct configuration
        collection_info = client.get_collection("chat_feedback")
        if collection_info.config.params.vectors.size != 768:
            raise ValueError("Incorrect vector size in 'chat_feedback' collection.")
        logging.info("'chat_feedback' collection verified successfully with correct vector size.")
        
        return True
    except Exception as e:
        logging.error(f"Failed to initialize feedback collection: {e}")
        raise

async def submit_feedback(feedback_type, chat_history, language_choice):
    """Submit feedback with improved error handling and logging."""
    try:
        if not chat_history or len(chat_history) < 2:
            logging.warning("Attempted to submit feedback with insufficient chat history.")
            return "No recent interaction to provide feedback for."

        # Get last question and answer
        last_interaction = chat_history[-2:]
        question = last_interaction[0].get("content", "").strip()
        answer = last_interaction[1].get("content", "").strip()

        if not question or not answer:
            logging.warning("Question or answer content is missing.")
            return "Incomplete interaction data. Cannot submit feedback."

        logging.info(f"Processing feedback for question: {question[:50]}...")

        # Initialize client
        client = initialize_database_client()

        # Create point ID
        point_id = str(uuid.uuid4())

        # Create payload
        payload = {
            "question": question,
            "answer": answer,
            "language": language_choice,
            "timestamp": datetime.utcnow().isoformat(),
            "feedback": feedback_type
        }

        # Initialize embeddings
        embeddings = FastEmbedEmbeddings(model_name="nomic-ai/nomic-embed-text-v1.5-Q")

        # Create embeddings for the Q&A pair
        try:
            embedding_text = f"{question} {answer}"
            vector = await asyncio.to_thread(embeddings.embed_query, embedding_text)
            logging.info(f"Generated embedding vector of length {len(vector)}.")
        except Exception as embed_error:
            logging.error(f"Embedding generation failed: {embed_error}")
            return "Failed to generate embeddings for your feedback."

        if not isinstance(vector, list) or not vector:
            logging.error("Invalid vector generated from embeddings.")
            return "Failed to generate valid embeddings for your feedback."

        # Create point
        point = PointStruct(
            id=point_id,
            payload=payload,
            vector=vector
        )

        # Store in Qdrant
        try:
            operation_info = await asyncio.to_thread(
                client.upsert,
                collection_name="chat_feedback",
                points=[point]
            )
            logging.info(f"Feedback submitted successfully: {point_id}")
            return "Thanks for your feedback! Your response has been recorded."
        except Exception as db_error:
            logging.error(f"Failed to upsert point to Qdrant: {db_error}")
            return "Sorry, there was an error submitting your feedback."

    except Exception as e:
        logging.error(f"Unexpected error in submit_feedback: {e}")
        return "Sorry, there was an unexpected error submitting your feedback."

# Initialize services and feedback collection
try:
    retriever, llm = initialize_services()
    initialize_feedback_collection()
except Exception as initialization_error:
    logging.critical(f"Initialization failed: {initialization_error}")
    raise

# Prompt template
prompt_template = PromptTemplate(
    template="""You are RRA Assistant, created by Cedric to help users get tax related information in Rwanda. Your task is to answer tax-related questions using the provided context.



Context: {context}



User's Question: {question}



Please follow these steps to answer the question:



Step 1: Analyze the question

Briefly explain your understanding of the question and any key points to address. If it is hi or hello, skip to step 3 and respond with a greeting.



Step 2: Provide relevant information

Using the context provided, give detailed information related to the question. Include specific facts, figures, or explanations from the context.



Step 3: Final answer

Provide a clear, concise answer to the original question. Start directly with the relevant information, avoiding phrases like "In summary" or "To conclude".



Remember:

- If you don't know the answer or can't find relevant information in the context, say so honestly.

- Do not make up information.

- Use the provided context to support your answer.

- Include "For more information, call 3004" at the end of every answer.



Your response:

""",
    input_variables=['context', 'question']
)

async def process_query(message: str, language: str, chat_history: list) -> str:
    try:
        # Handle translation based on selected language
        if language == "Kinyarwanda":
            query = translator.translate(message, "rw", "en")
            logging.info(f"Translated query to English: {query}")
        else:
            query = message
        
        # Create QA chain
        qa = RetrievalQA.from_chain_type(
            llm=llm,
            chain_type="stuff",
            retriever=retriever,
            chain_type_kwargs={"prompt": prompt_template},
            return_source_documents=True
        )
        
        # Get response
        response = await asyncio.to_thread(
            lambda: qa.invoke({"query": query})
        )
        logging.info("QA chain invoked successfully.")
        
        # Extract final answer
        result_text = response.get('result', '')
        final_answer_start = result_text.find("Step 3: Final answer")
        if final_answer_start != -1:
            answer = result_text[final_answer_start + len("Step 3: Final answer"):].strip()
        else:
            answer = result_text
        
        # Clean up the answer
        answer = re.sub(r'\*\*', '', answer).strip()
        answer = re.sub(r'Step \d+:', '', answer).strip()
        
        # Translate response if needed
        if language == "Kinyarwanda":
            answer = translator.translate(answer, "en", "rw")
            logging.info(f"Translated answer to Kinyarwanda: {answer}")
        
        return answer
    except Exception as e:
        logging.error(f"Query processing error: {str(e)}")
        return f"An error occurred: {str(e)}"

# Define separate feedback submission functions to pass feedback type correctly
async def submit_positive_feedback(chat_history, language_choice):
    return await submit_feedback("positive", chat_history, language_choice)

async def submit_negative_feedback(chat_history, language_choice):
    return await submit_feedback("negative", chat_history, language_choice)

# Create Gradio interface
with gr.Blocks(title="RRA FAQ Chatbot") as demo:
    gr.Markdown(
        """

        # RRA FAQ Chatbot

        Ask tax-related questions in English or Kinyarwanda

        > πŸ”’ Your questions and interactions remain private unless you choose to submit feedback, which helps improve our service.

        """
    )
    
    # Add language selector
    language = gr.Radio(
        choices=["English", "Kinyarwanda"],
        value="English",
        label="Select Language / Hitamo Ururimi"
    )
    
    chatbot = gr.Chatbot(
        value=[],
        show_label=False,
        height=400,
        type='messages'
    )
    
    with gr.Row():
        msg = gr.Textbox(
            label="Ask your question",
            placeholder="Type your tax-related question here...",
            show_label=False
        )
        submit = gr.Button("Send")
    
    # Add feedback section
    with gr.Row():
        with gr.Column(scale=2):
            feedback_label = gr.Markdown("Was this response helpful?")
        with gr.Column(scale=1):
            feedback_positive = gr.Button("πŸ‘ Helpful")
        with gr.Column(scale=1):
            feedback_negative = gr.Button("πŸ‘Ž Not Helpful")
    
    # Add feedback status message
    feedback_status = gr.Markdown("")
    
    # Connect feedback buttons to their respective functions
    feedback_positive.click(
        fn=submit_positive_feedback,
        inputs=[chatbot, language],
        outputs=feedback_status
    )
    
    feedback_negative.click(
        fn=submit_negative_feedback,
        inputs=[chatbot, language],
        outputs=feedback_status
    )
    
    # Create two sets of examples
    with gr.Row() as english_examples_row:
        gr.Examples(
            examples=[
                "What is VAT in Rwanda?",
                "How do I register for taxes?",
                "What are the tax payment deadlines?",
                "How can I get a TIN number?",
                "How do I get purchase code?"
            ],
            inputs=msg,
            label="English Examples"
        )
    
    with gr.Row(visible=False) as kinyarwanda_examples_row:
        gr.Examples(
            examples=[
                "Ese VAT ni iki mu Rwanda?",
                "Nabona TIN number nte?",
                "Ni ryari tugomba kwishyura imisoro?",
                "Ese nandikwa nte ku musoro?",
                "Ni gute nabone kode yo kugura?"
            ],
            inputs=msg,
            label="Kinyarwanda Examples"
        )
    
    async def respond(message, lang, chat_history):
        bot_message = await process_query(message, lang, chat_history)
        chat_history.append({"role": "user", "content": message})
        chat_history.append({"role": "assistant", "content": bot_message})
        return "", chat_history
    
    def toggle_language_interface(language_choice):
        if language_choice == "English":
            placeholder_text = "Type your tax-related question here..."
            return {
                msg: gr.update(placeholder=placeholder_text),
                english_examples_row: gr.update(visible=True),
                kinyarwanda_examples_row: gr.update(visible=False)
            }
        else:
            placeholder_text = "Andika ibibazo bijyanye n'umusoro hano"
            return {
                msg: gr.update(placeholder=placeholder_text),
                english_examples_row: gr.update(visible=False),
                kinyarwanda_examples_row: gr.update(visible=True)
            }
    
    msg.submit(respond, [msg, language, chatbot], [msg, chatbot])
    submit.click(respond, [msg, language, chatbot], [msg, chatbot])
    
    # Update both examples visibility and placeholder when language changes
    language.change(
        fn=toggle_language_interface,
        inputs=language,
        outputs=[msg, english_examples_row, kinyarwanda_examples_row]
    )
    
    gr.Markdown(
        """

        ### About

        - Created by: [Cedric](mailto:[email protected])

        - Data source: [RRA Website FAQ](https://www.rra.gov.rw/en/domestic-tax-services/faqs)

        

        **Disclaimer:** This chatbot provides general tax information. For official guidance, 

        consult RRA or call 3004.

        πŸ”’ **Privacy:** Your interactions remain private unless you choose to submit feedback.

        """
    )

# Launch the app
if __name__ == "__main__":
    try:
        demo.launch(share=False)
        logging.info("Gradio app launched successfully.")
    except Exception as launch_error:
        logging.critical(f"Failed to launch Gradio app: {launch_error}")
        raise