import os import requests from dotenv import load_dotenv import logging load_dotenv() logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) class LLMExplainer: def __init__(self): self.api_key = os.environ.get("MISTRAL_API_KEY") self.api_url = "https://api.mistral.ai/v1/chat/completions" self.explanation_cache = {} def explain_discrepancy(self, row, match_status, books_vendor, gst_vendor, books_amount, gst_amount): """ Generates a human-readable explanation for a discrepancy using Mistral AI. """ if not self.api_key: return "API Key not configured. Discrepancy: " + match_status is_anomaly = row.get('IsAnomaly', False) if match_status == "Exact Match" and not is_anomaly: return "Records match perfectly." cache_key = (match_status, books_vendor, gst_vendor, books_amount, gst_amount, is_anomaly) if cache_key in self.explanation_cache: logger.info("Returning cached LLM explanation.") return self.explanation_cache[cache_key] prompt = f""" You are an expert AI financial auditor. Explain the following transaction in 1-2 concise, professional sentences. Provide a recommended action. Reconciliation Status: {match_status} Is Flagged as Anomaly: {is_anomaly} Anomaly Score: {row.get('AnomalyScore', 0.0)} Invoice ID: {row.get('InvoiceID', 'Unknown')} Books Vendor: {books_vendor} GST Vendor: {gst_vendor} Books Amount: {books_amount} GST Amount: {gst_amount} Explanation and Recommendation: """ headers = { "Content-Type": "application/json", "Accept": "application/json", "Authorization": f"Bearer {self.api_key}" } data = { "model": "mistral-tiny", # Using tiny for faster responses, can upgrade to small/medium "messages": [ {"role": "user", "content": prompt} ], "temperature": 0.3 } try: response = requests.post(self.api_url, headers=headers, json=data, timeout=10) response.raise_for_status() result = response.json() explanation = result['choices'][0]['message']['content'].strip() self.explanation_cache[cache_key] = explanation return explanation except requests.exceptions.RequestException as e: logger.error(f"Error calling Mistral API: {e}") return f"API Error ({match_status}). Please check amounts and vendor names manually." except (KeyError, IndexError) as e: logger.error(f"Unexpected response format from Mistral API: {e}") return f"Error parsing AI response ({match_status})." def generate_explanations_batch(self, discrepancies_df): """ Generates explanations for a dataframe of discrepancies. """ explanations = [] for _, row in discrepancies_df.iterrows(): status = row.get('MatchStatus', 'Unknown') b_vendor = row.get('VendorName_books', 'N/A') g_vendor = row.get('VendorName_gst', 'N/A') b_amount = row.get('Amount_books', 0) g_amount = row.get('Amount_gst', 0) explanation = self.explain_discrepancy(row, status, b_vendor, g_vendor, b_amount, g_amount) explanations.append(explanation) return explanations