Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/bhanu221001-ship-it_sap-migration-bot.json

1 line
36 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "bhanu221001-ship-it/sap-migration-bot", "files": {"app.py": {"31420c5edef6ef44596483e12f44f0c4d86d121d": {"t": 1785674583.185048, "n": 34279, "h": "d2102125439cfc2d", "c": "# ============================================================\n# SAP DATA MIGRATION VALIDATION BOT\n# Uses: Python + Streamlit + Pandas + Google Gemini AI (FREE)\n# ============================================================\n# HOW TO RUN THIS FILE:\n# 1. Open terminal in VS Code (press Ctrl + backtick)\n# 2. Type: cd sap_migration_bot\n# 3. Type: streamlit run app.py\n# 4. Your browser opens automatically at http://localhost:8501\n# ============================================================\n\n# ── IMPORTS ──────────────────────────────────────────────────\n# These are the tools (libraries) we are loading into our program.\n# Think of them like apps you open before doing work.\n\nimport streamlit as st # Builds the web page UI (buttons, tables, file uploaders)\nimport pandas as pd # Reads Excel/CSV files and compares data like a spreadsheet\nimport smtplib # Sends emails — already included in Python, no install needed\nimport io # Lets us work with files in memory (no saving to disk)\nfrom email.mime.text import MIMEText\nfrom email.mime.multipart import MIMEMultipart\nimport google.generativeai as genai # Google Gemini AI — for smart fix suggestions (FREE)\n\n\n# ── SECTION 1: PAGE CONFIGURATION ────────────────────────────\n# This sets up the browser tab title, icon, and layout.\n# 'layout=\"wide\"' uses the full screen width.\n\nst.set_page_config(\n page_title=\"SAP Migration Validator\",\n page_icon=\"🔄\",\n layout=\"wide\"\n)\n\n\n# ── SECTION 2: PAGE TITLE ─────────────────────────────────────\n# This is what appears at the very top of your app page.\n\nst.title(\"🔄 SAP Data Migration Validation Bot\")\nst.caption(\"Powered by Google Gemini AI — Free Tier | No paid subscriptions needed\")\nst.markdown(\"\"\"\nWelcome! This tool validates your **SAP R/3 → S/4HANA** migration.\n\n**How it works in 5 steps:**\n1. Upload your 3 files (Old Data, New Data, Mapping Rules)\n2. Click Run — the bot checks every record automatically\n3. View your report (pass/fail per record + why it failed)\n4. Download the Excel report\n5. Email the remediation team (only if YOU approve it)\n\"\"\")\nst.divider()\n\n\n# ── SECTION 3: SIDEBAR ───────────────────────────────────────\n# The sidebar is the LEFT panel. We put all settings here.\n# Users fill this in once before using the tool.\n\nst.sidebar.header(\"⚙️ Settings\")\n\n# --- Gemini AI API Key ---\n# The user pastes their free Gemini key here.\n# type=\"password\" hides the characters (like a password box).\nst.sidebar.subheader(\"🤖 Step A — Gemini AI Key (Free)\")\ngemini_api_key = st.sidebar.text_input(\n \"Paste your Gemini API Key here\",\n type=\"password\",\n placeholder=\"AIzaSy...\",\n help=\"Get it FREE at aistudio.google.com → Get API Key. No credit card needed.\"\n)\nst.sidebar.caption(\"🔗 [Click here to get your free key](https://aistudio.google.com)\")\n\nst.sidebar.divider()\n\n# --- Email Settings ---\n# Only needed in Step 5 when the user wants to email the team.\n# Users can skip this for now and fill it in later.\nst.sidebar.subheader(\"📧 Step B — Email Settings (Optional)\")\nst.sidebar.caption(\"Fill this in only when you want to email the team in Step 5\")\n\nsender_email = st.sidebar.text_input(\n \"Your Gmail Address\",\n placeholder=\"you@gmail.com\"\n)\nsender_password = st.sidebar.text_input(\n \"Your Gmail App Password\",\n type=\"password\",\n help=\"\"\"\n IMPORTANT: This is NOT your regular Gmail password.\n How to get it:\n 1. Go to myaccount.google.com\n 2. Click Security\n 3. Click 2-Step Verification (must be ON)\n 4. Scroll down → App Passwords\n 5. Generate one for 'Mail'\n 6. Copy the 16-character code and paste it here\n \"\"\"\n)\nrecipient_email = st.sidebar.text_input(\n \"Remediation Team Email\",\n placeholder=\"team@company.com\"\n)\n\nst.sidebar.divider()\nst.sidebar.success(\"✅ All tools used are 100% FREE\")\nst.sidebar.info(\"💡 Data uploaded here is processed in memory only — never stored anywhere.\")\n\n\n# ── SECTION 4: FILE UPLOAD ────────────────────────────────────\n# Three file uploaders placed side by side in 3 columns.\n# Users upload their SAP data files here.\n\nst.header(\"📁 Step 1: Upload Your Three Files\")\n\n# Create 3 equal columns side by side on the page\ncol1, col2, col3 = st.columns(3)\n\nwith col1:\n st.subheader(\"1️⃣ Old Data (SAP R/3)\")\n st.caption(\"Your data BEFORE migration happened\")\n old_file = st.file_uploader(\n \"Upload Old Data file\",\n type=[\"csv\", \"xlsx\"],\n key=\"old_data\",\n help=\"This is your SAP R/3 data export\"\n )\n # Show a green tick if file is uploaded, red message if not\n if old_file:\n st.success(f\"✅ Loaded: {old_file.name}\")\n\nwith col2:\n st.subheader(\"2️⃣ New Data (S/4HANA)\")\n st.caption(\"Your data AFTER migration happened\")\n new_file = st.file_uploader(\n \"Upload New Data file\",\n type=[\"csv\", \"xlsx\"],\n key=\"new_data\",\n help=\"This is your S/4HANA data export\"\n )\n if new_file:\n st.success(f\"✅ Loaded: {new_file.name}\")\n\nwith col3:\n st.subheader(\"3️⃣ Mapping Rules\")\n st.caption(\"The rules that say what should change\")\n mapping_file = st.file_uploader(\n \"Upload Mapping Rules file\",\n type=[\"csv\", \"xlsx\"],\n key=\"mapping_rules\",\n help=\"Must have columns: Field_Name, Old_Value, New_Value\"\n )\n if mapping_file:\n st.success(f\"✅ Loaded: {mapping_file.name}\")\n\n# Collapsible help section showing file format examples\nwith st.expander(\"ℹ️ What format should my files be in? Click here to see examples\"):\n st.markdown(\"\"\"\n ---\n ### Old Data & New Data files\n Both files must have a **common key column** — a column that exists in BOTH files\n and uniquely identifies each record (like a Material Number or Vendor ID).\n\n **Example Old Data (R/3):**\n | Material_Number | Plant_Code | Status | Description |\n |---|---|---|---|\n | MAT001 | 1000 | ZOLD | Widget A |\n | MAT002 | 2000 | ZOLD | Widget B |\n\n **Example New Data (S/4HANA):**\n | Material_Number | Plant_Code | Status | Description |\n |---|---|---|---|\n | MAT001 | US01 | ZNEW | Widget A |\n | MAT002 | US02 | ZNEW | Widget B |\n\n ---\n ### Mapping Rules file\n Must have **EXACTLY** these 3 column names (copy them exactly — they are case-sensitive):\n\n | Field_Name | Old_Value | New_Value |\n |---|---|---|\n | Plant_Code | 1000 | US01 |\n | Plant_Code | 2000 | US02 |\n | Status | ZOLD | ZNEW |\n | Company_Code | 100 | US10 |\n\n The bot will check: for every record, did Plant_Code '1000' correctly become 'US01'?\n If not → it flags it as a Rule Violation failure.\n \"\"\")\n\nst.divider()\n\n\n# ── SECTION 5: HELPER FUNCTIONS ──────────────────────────────\n# Functions are reusable blocks of code.\n# We define them here and call them later when the button is clicked.\n# Think of functions like recipes — you write the recipe once,\n# then you can cook it (call it) as many times as you need.\n\ndef load_file(uploaded_file):\n \"\"\"\n WHAT THIS DOES:\n Takes an uploaded file (CSV or Excel) and reads it into a\n Pandas DataFrame. A DataFrame is like a table/spreadsheet in memory.\n\n WHY WE NEED IT:\n Python can't directly understand Excel files — Pandas reads them\n and turns them into a table we can work with in code.\n \"\"\"\n if uploaded_file is None:\n return None\n\n # Check the file extension so we know which reader to use\n if uploaded_file.name.endswith(\".csv\"):\n return pd.read_csv(uploaded_file) # Read CSV file\n else:\n return pd.read_excel(uploaded_file) # Read Excel (.xlsx) file\n\n\ndef validate_data(old_df, new_df, mapping_df, key_col):\n \"\"\"\n WHAT THIS DOES:\n This is the CORE ENGINE of the entire bot.\n It compares old data vs new data and checks if mapping rules were followed.\n\n HOW IT WORKS:\n 1. Builds a lookup dictionary from the mapping rules\n (so we can quickly ask: \"what should Plant_Code 1000 become?\")\n 2. Merges old and new data side-by-side using the key column\n 3. For every record, runs 5 checks\n 4. Returns a results table with Status and Failure_Reasons for each record\n\n THE 5 CHECKS:\n Check 1 — Is this record missing from the new S/4HANA data?\n Check 2 — Is there a surprise new record with no R/3 source?\n Check 3 — Did the mapping rule get applied correctly?\n Check 4 — Is there a blank/null value in the new data?\n Check 5 — Was any value truncated (cut shorter than expected)?\n \"\"\"\n\n # ── BUILD THE RULES LOOKUP DICTIONARY ──\n # We turn the mapping rules table into a Python dictionary\n # so we can quickly look up: \"if field X had old value Y, it should now be Z\"\n #\n # Example result:\n # rules = {\n # \"Plant_Code\": {\"1000\": \"US01\", \"2000\": \"US02\"},\n # \"Status\": {\"ZOLD\": \"ZNEW\"}\n # }\n rules = {}\n for _, row in mapping_df.iterrows():\n field = str(row[\"Field_Name\"]).strip() # e.g. \"Plant_Code\"\n old_val = str(row[\"Old_Value\"]).strip() # e.g. \"1000\"\n new_val = str(row[\"New_Value\"]).strip() # e.g. \"US01\"\n rules.setdefault(field, {})[old_val] = new_val\n\n # ── MERGE OLD AND NEW DATA SIDE BY SIDE ──\n # This joins the two tables so each row has BOTH the old and new values.\n # We use \"outer\" join so we keep ALL records, even unmatched ones.\n # \"indicator=True\" adds a \"_merge\" column that tells us:\n # \"both\" → record exists in both old and new (normal)\n # \"left_only\" → record only in old data (MISSING from new!)\n # \"right_only\" → record only in new data (unexpected extra!)\n #\n # suffixes add \"_old\" and \"_new\" to column names so we can tell them apart.\n # Example: \"Plant_Code\" becomes \"Plant_Code_old\" and \"Plant_Code_new\"\n merged = old_df.merge(\n new_df,\n on=key_col,\n how=\"outer\",\n suffixes=(\"_old\", \"_new\"),\n indicator=True\n )\n\n results = [] # We'll collect one result dictionary per record here\n\n # ── LOOP THROUGH EVERY RECORD AND RUN ALL 5 CHECKS ──\n for _, row in merged.iterrows():\n status = \"✅ Correct\" # Start assuming it's fine\n reasons = [] # List to collect any problems found\n\n # ── CHECK 1: Is this record missing from new data? ──\n if row[\"_merge\"] == \"left_only\":\n status = \"❌ Failed\"\n reasons.append(\n \"Record exists in old R/3 data but is MISSING from new S/4HANA data after migration.\"\n )\n\n # ── CHECK 2: Is there an unexpected extra record in new data? ──\n elif row[\"_merge\"] == \"right_only\":\n status = \"⚠️ Warning\"\n reasons.append(\n \"Record found in S/4HANA but has NO matching entry in original R/3 data.\"\n )\n\n else:\n # Record exists in both — now check each field against mapping rules\n\n for field, transforms in rules.items():\n # Build the column names with their suffixes\n old_col = f\"{field}_old\"\n new_col = f\"{field}_new\"\n\n # Skip if this field doesn't exist in the merged data\n if old_col not in row.index or new_col not in row.index:\n continue\n\n # Get the actual values, handling missing/null values\n old_val = str(row[old_col]).strip() if pd.notna(row[old_col]) else \"BLANK\"\n new_val = str(row[new_col]).strip() if pd.notna(row[new_col]) else \"BLANK\"\n\n # ── CHECK 3: Was the mapping rule applied correctly? ──\n # If a rule says \"1000 should become US01\", check if it did.\n if old_val in transforms:\n expected_new_val = transforms[old_val]\n if new_val != expected_new_val:\n status = \"❌ Failed\"\n reasons.append(\n f\"Rule Violation on '{field}': \"\n f\"Expected '{expected_new_val}' but found '{new_val}'. \"\n f\"(Old value was '{old_val}')\"\n )\n\n # ── CHECK 4: Is the new value blank or empty? ──\n if new_val in (\"BLANK\", \"nan\", \"None\", \"\", \"NaN\"):\n if status == \"✅ Correct\":\n status = \"❌ Failed\"\n reasons.append(\n f\"Blank/null value in field '{field}' in new S/4HANA data. \"\n f\"The old R/3 value was '{old_val}'.\"\n )\n\n # ── CHECK 5: Was the value possibly truncated? ──\n # If the new value is less than HALF the length of the old value,\n # it may have been accidentally cut short (e.g. leading zeros stripped).\n if (len(old_val) > 3 and\n len(new_val) > 0 and\n len(new_val) < len(old_val) // 2):\n if status == \"✅ Correct\":\n status = \"⚠️ Warning\"\n reasons.append(\n f\"Possible truncation in '{field}': \"\n f\"Old value='{old_val}' ({len(old_val)} chars) → \"\n f\"New value='{new_val}' ({len(new_val)} chars). \"\n f\"Check for leading-zero stripping.\"\n )\n\n # Add this record's result to our results list\n results.append({\n key_col: row[key_col],\n \"Status\": status,\n \"Failure_Reasons\": \" | \".join(reasons) if reasons else \"None — all checks passed\"\n })\n\n # Convert our list of results into a DataFrame (table) and return it\n return pd.DataFrame(results)\n\n\ndef get_gemini_suggestions(error_summary, api_key):\n \"\"\"\n WHAT THIS DOES:\n Sends the error summary to Google Gemini AI and gets back\n expert SAP remediation advice in plain English.\n\n WHY GEMINI:\n Gemini Flash is Google's free AI model.\n Free tier allows up to 1 million tokens per day — more than enough.\n No credit card needed. Just a Google account.\n\n HOW IT WORKS:\n 1. We configure Gemini with the user's API key\n 2. We write a detailed prompt telling Gemini who it is and what we need\n 3. We send the error summary to Gemini\n 4. Gemini responds with expert SAP advice\n 5. We return that advice to display on the page\n \"\"\"\n if not api_key:\n return (\n \"⚠️ No Gemini API key found.\\n\\n\"\n \"To get AI suggestions:\\n\"\n \"1. Go to aistudio.google.com\\n\"\n \"2. Sign in with your Google account\\n\"\n \"3. Click 'Get API Key' → 'Create API Key'\\n\"\n \"4. Paste the key in the left sidebar under 'Gemini API Key'\"\n )\n\n try:\n # Tell Gemini what our API key is\n genai.configure(api_key=api_key)\n\n # Choose the model — gemini-1.5-flash is the FREE, fast model\n model = genai.GenerativeModel(\"gemini-1.5-flash\")\n\n # Write the prompt — this is the instruction we give Gemini\n # The better our prompt, the better Gemini's answer\n prompt = f\"\"\"\nYou are a Senior SAP Data Migration Specialist with 20 years of experience\nspecifically in SAP R/3 to S/4HANA migration projects.\n\nA data validation bot has finished checking a migration and found the following errors:\n\n--- ERROR SUMMARY ---\n{error_summary}\n--- END OF ERRORS ---\n\nPlease provide a detailed response with the following 4 sections:\n\nSECTION 1 — ROOT CAUSE ANALYSIS\nExplain in plain English what most likely went wrong in the migration process\nto cause each type of error found.\n\nSECTION 2 — STEP-BY-STEP REMEDIATION\nFor each error type, provide specific steps to fix it.\nInclude the exact SAP transaction codes (like SE16N, MM60, LSMW, etc.)\nthat can be used to verify and correct the data.\n\nSECTION 3 — HOW TO PREVENT THIS NEXT TIME\nList 3-5 best practices to prevent each error type in future migration runs.\n\nSECTION 4 — PRIORITY ORDER\nTell us which errors to fix first and why.\n\nWrite in plain English so a business analyst (not just a developer) can\nunderstand and act on your advice. Be specific and practical.\n \"\"\"\n\n # Call the Gemini API and get the response\n response = model.generate_content(prompt)\n\n # Return the text of Gemini's response\n return response.text\n\n except Exception as e:\n # If something goes wrong (wrong key, network issue, etc.)\n # return a helpful error message instead of crashing\n return (\n f\"❌ Gemini API Error: {str(e)}\\n\\n\"\n \"Common fixes:\\n\"\n \"• Check that your API key is correct (starts with 'AIza')\\n\"\n \"• Make sure you have internet connection\\n\"\n \"• Try generating a new key at aistudio.google.com\"\n )\n\n\ndef send_email(sender, password, recipient, subject, body):\n \"\"\"\n WHAT THIS DOES:\n Sends an email using Gmail's SMTP server.\n SMTP is the standard internet protocol for sending emails.\n\n HOW IT WORKS:\n 1. Creates the email message object (From, To, Subject, Body)\n 2. Connects to Gmail's email server at smtp.gmail.com on port 587\n 3. Encrypts the connection with TLS (for security)\n 4. Logs in with the user's credentials\n 5. Sends the email\n 6. Returns True if it worked, False if something failed\n\n WHY PORT 587:\n This is the standard \"submission\" port for sending email securely.\n Port 587 with STARTTLS is the recommended way to send via Gmail.\n \"\"\"\n try:\n # Create the email container object\n msg = MIMEMultipart()\n msg[\"From\"] = sender # Who is sending\n msg[\"To\"] = recipient # Who receives it\n msg[\"Subject\"] = subject # Email subject line\n\n # Add the body text to the email\n # \"plain\" means plain text format (not HTML)\n msg.attach(MIMEText(body, \"plain\"))\n\n # Connect to Gmail and send\n with smtplib.SMTP(\"smtp.gmail.com\", 587) as server:\n server.starttls() # Encrypt the connection\n server.login(sender, password) # Log in to Gmail\n server.sendmail(sender, recipient, msg.as_string()) # Send!\n\n return True, \"✅ Email sent successfully! Check the recipient's inbox.\"\n\n except smtplib.SMTPAuthenticationError:\n return False, (\n \"❌ Gmail login failed.\\n\"\n \"Make sure you are using an App Password (not your regular Gmail password).\\n\"\n \"Go to myaccount.google.com → Security → App Passwords to create one.\"\n )\n except Exception as e:\n return False, f\"❌ Email failed: {str(e)}\"\n\n\n# ── SECTION 6: RUN VALIDATION BUTTON ─────────────────────────\n# This is the main action button of the app.\n# When the user clicks it, all the validation logic runs.\n\nst.header(\"🚀 Step 2: Configure & Run Validation\")\n\n# Ask the user for the key column name\nkey_column = st.text_input(\n \"🔑 Enter the KEY column name — the column that exists in BOTH your Old and New files\",\n placeholder=\"e.g. Material_Number or Vendor_ID or Employee_Number\",\n help=(\n \"This is the unique identifier linking each old record to its new version. \"\n \"It must be EXACTLY the same column name in both files. \"\n \"Column names are case-sensitive — 'Material_Number' ≠ 'material_number'.\"\n )\n)\n\n# Show a preview of uploaded files so users can verify before running\nif old_file and new_file and mapping_file:\n with st.expander(\"👀 Preview your uploaded files before running (click to expand)\"):\n tab_a, tab_b, tab_c = st.tabs([\"Old Data (R/3)\", \"New Data (S/4HANA)\", \"Mapping Rules\"])\n with tab_a:\n preview_old = load_file(old_file)\n if preview_old is not None:\n st.dataframe(preview_old.head(5), use_container_width=True)\n st.caption(f\"Showing first 5 rows. Total: {len(preview_old)} rows, {len(preview_old.columns)} columns\")\n with tab_b:\n preview_new = load_file(new_file)\n if preview_new is not None:\n st.dataframe(preview_new.head(5), use_container_width=True)\n st.caption(f\"Showing first 5 rows. Total: {len(preview_new)} rows, {len(preview_new.columns)} columns\")\n with tab_c:\n preview_map = load_file(mapping_file)\n if preview_map is not None:\n st.dataframe(preview_map, use_container_width=True)\n st.caption(f\"Total mapping rules: {len(preview_map)}\")\n\n# THE BIG RUN BUTTON\n# type=\"primary\" makes it the main green/blue button\n# use_container_width=True makes it stretch across the full page width\nif st.button(\"▶️ Run Validation Now\", type=\"primary\", use_container_width=True):\n\n # ── GUARD CHECKS ──\n # Before running, make sure everything is in order.\n # We check each requirement and show a specific error if something is missing.\n\n if not old_file:\n st.error(\"❌ Missing: Please upload the Old Data (SAP R/3) file above.\")\n st.stop()\n\n if not new_file:\n st.error(\"❌ Missing: Please upload the New Data (S/4HANA) file above.\")\n st.stop()\n\n if not mapping_file:\n st.error(\"❌ Missing: Please upload the Mapping Rules file above.\")\n st.stop()\n\n if not key_column.strip():\n st.error(\"❌ Missing: Please type the key column name in the box above.\")\n st.stop()\n\n # ── RUN THE VALIDATION ──\n # The spinner shows a loading animation while the code runs\n with st.spinner(\"⏳ Reading your files and running all 5 validation checks... please wait.\"):\n\n # Load all 3 files into DataFrames (Python tables)\n old_df = load_file(old_file)\n new_df = load_file(new_file)\n mapping_df = load_file(mapping_file)\n\n # Check that the mapping file has the required column names\n required_cols = {\"Field_Name\", \"Old_Value\", \"New_Value\"}\n if not required_cols.issubset(set(mapping_df.columns)):\n st.error(\n f\"❌ Your Mapping Rules file is missing required columns.\\n\\n\"\n f\"It must have these EXACT column names: **Field_Name**, **Old_Value**, **New_Value**\\n\\n\"\n f\"Your file has: {list(mapping_df.columns)}\"\n )\n st.stop()\n\n # Check that the key column exists in the Old Data file\n if key_column not in old_df.columns:\n st.error(\n f\"❌ Column '{key_column}' not found in Old Data file.\\n\\n\"\n f\"Your Old Data has these columns: {list(old_df.columns)}\\n\\n\"\n f\"Note: Column names are case-sensitive.\"\n )\n st.stop()\n\n # Check that the key column exists in the New Data file\n if key_column not in new_df.columns:\n st.error(\n f\"❌ Column '{key_column}' not found in New Data file.\\n\\n\"\n f\"Your New Data has these columns: {list(new_df.columns)}\\n\\n\"\n f\"Note: Column names are case-sensitive.\"\n )\n st.stop()\n\n # ── ALL CHECKS PASSED — RUN THE VALIDATION ENGINE ──\n results_df = validate_data(old_df, new_df, mapping_df, key_column)\n\n # Build a readable summary of which rules were loaded\n rules_loaded = \", \".join([\n f\"{r['Field_Name']}: '{r['Old_Value']}' → '{r['New_Value']}'\"\n for _, r in mapping_df.iterrows()\n ])\n\n # ── SAVE TO SESSION STATE ──\n # Streamlit re-runs the entire script on every button click.\n # session_state is how we save data between those re-runs.\n # Without this, clicking \"Download\" would erase the results.\n st.session_state[\"results_df\"] = results_df\n st.session_state[\"rules_loaded\"] = rules_loaded\n st.session_state[\"key_column\"] = key_column\n st.session_state[\"done\"] = True\n\n # Show success message (only reaches here if no errors occurred)\n st.success(\"✅ Validation complete! Your full report is ready below.\")\n st.balloons() # Fun celebration animation 🎈\n\nst.divider()\n\n\n# ── SECTION 7: REPORT DASHBOARD ──────────────────────────────\n# This entire section only appears AFTER the validation has been run.\n# We check session_state to know if validation is done.\n\nif st.session_state.get(\"done\"):\n\n results_df = st.session_state[\"results_df\"]\n rules_loaded = st.session_state.get(\"rules_loaded\", \"\")\n key_column = st.session_state.get(\"key_column\", \"\")\n\n st.header(\"📊 Step 3: Your Validation Report\")\n\n # ── CALCULATE SUMMARY NUMBERS ──\n total = len(results_df)\n correct = len(results_df[results_df[\"Status\"] == \"✅ Correct\"])\n failed = len(results_df[results_df[\"Status\"] == \"❌ Failed\"])\n warnings = len(results_df[results_df[\"Status\"] == \"⚠️ Warning\"])\n pass_rate = round(correct / total * 100, 1) if total > 0 else 0\n\n # ── SHOW THE 4 BIG KPI CARDS ──\n # These are the big number boxes at the top of the dashboard\n c1, c2, c3, c4 = st.columns(4)\n c1.metric(\n label=\"📦 Total Records Checked\",\n value=total\n )\n c2.metric(\n label=\"✅ Correctly Migrated\",\n value=correct,\n delta=f\"{pass_rate}% pass rate\"\n )\n c3.metric(\n label=\"❌ Failed\",\n value=failed,\n delta=f\"{failed} need fixing\" if failed > 0 else \"None!\",\n delta_color=\"inverse\"\n )\n c4.metric(\n label=\"⚠️ Warnings\",\n value=warnings,\n delta=f\"{warnings} to review\" if warnings > 0 else \"None!\",\n delta_color=\"off\"\n )\n\n # Show what rules were checked\n st.caption(f\"**Rules validated:** {rules_loaded}\")\n st.caption(\"✅ Correct = all 5 checks passed | ❌ Failed = violation found | ⚠️ Warning = needs manual review\")\n\n st.divider()\n\n # ── THREE TABS FOR DIFFERENT VIEWS ──\n # Tab 1: Full results table (all records)\n # Tab 2: Only failed/warning records (the ones needing attention)\n # Tab 3: AI-generated fix suggestions from Gemini\n\n tab1, tab2, tab3 = st.tabs([\n \"📋 All Records\",\n \"❌ Failures & Warnings Only\",\n \"🤖 AI Fix Suggestions (Gemini)\"\n ])\n\n with tab1:\n st.subheader(\"All Validated Records\")\n st.dataframe(results_df, use_container_width=True, height=400)\n st.caption(f\"Total: {total} records\")\n\n with tab2:\n st.subheader(\"Records That Need Your Attention\")\n # Filter to only show failed and warning records\n failures_df = results_df[results_df[\"Status\"] != \"✅ Correct\"].copy()\n\n if failures_df.empty:\n st.success(\"🎉 Amazing! Zero failures and zero warnings. Your migration is clean!\")\n else:\n st.dataframe(failures_df, use_container_width=True, height=400)\n st.caption(f\"Showing {len(failures_df)} records that need attention out of {total} total\")\n\n # Save failures to session state so we can use it later in email\n st.session_state[\"failures_df\"] = failures_df\n\n with tab3:\n st.subheader(\"🤖 AI-Powered SAP Remediation Advice — Google Gemini (Free)\")\n\n failures_df = results_df[results_df[\"Status\"] != \"✅ Correct\"].copy()\n\n if failures_df.empty:\n st.success(\"✅ No failures found — no AI analysis needed! Your migration is clean.\")\n else:\n st.info(\n f\"Found {len(failures_df)} records with issues. \"\n \"Click the button below to get expert SAP remediation advice from Gemini AI.\"\n )\n\n if st.button(\"🔍 Generate AI Suggestions from Gemini\", type=\"secondary\"):\n with st.spinner(\"🤖 Asking Gemini AI for expert SAP advice... this takes 10-20 seconds.\"):\n # Build the error summary to send to Gemini\n error_summary = failures_df[[\"Status\", \"Failure_Reasons\"]].to_string(index=False)\n # Call the Gemini function we defined earlier\n suggestions = get_gemini_suggestions(error_summary, gemini_api_key)\n # Save suggestions to session state\n st.session_state[\"ai_suggestions\"] = suggestions\n\n # Show the suggestions if they've been generated\n if \"ai_suggestions\" in st.session_state:\n st.markdown(\"---\")\n st.markdown(st.session_state[\"ai_suggestions\"])\n\n st.divider()\n\n\n # ── SECTION 8: EXPORT TO EXCEL ───────────────────────────\n # Creates a downloadable Excel file with 3 sheets:\n # Sheet 1: Summary numbers\n # Sheet 2: All results\n # Sheet 3: Failures only (if any)\n\n st.header(\"📥 Step 4: Download Your Report\")\n\n # Build the Excel file in memory (not saved to disk — just in RAM)\n # BytesIO is like a virtual file that exists only in memory\n excel_buffer = io.BytesIO()\n\n with pd.ExcelWriter(excel_buffer, engine=\"openpyxl\") as writer:\n\n # Sheet 1: Summary\n summary_data = {\n \"Metric\": [\n \"Total Records Processed\",\n \"Correctly Migrated\",\n \"Failed Records\",\n \"Warnings\",\n \"Pass Rate\",\n \"Rules Validated\",\n \"Key Column Used\"\n ],\n \"Value\": [\n total,\n correct,\n failed,\n warnings,\n f\"{pass_rate}%\",\n rules_loaded,\n key_column\n ]\n }\n pd.DataFrame(summary_data).to_excel(writer, sheet_name=\"Summary\", index=False)\n\n # Sheet 2: All results\n results_df.to_excel(writer, sheet_name=\"All Results\", index=False)\n\n # Sheet 3: Failures only (only add if there are failures)\n failures_df = results_df[results_df[\"Status\"] != \"✅ Correct\"]\n if not failures_df.empty:\n failures_df.to_excel(writer, sheet_name=\"Failures Only\", index=False)\n\n # Show the download button\n # Clicking this saves the Excel file to the user's computer\n st.download_button(\n label=\"⬇️ Download Full Validation Report (.xlsx)\",\n data=excel_buffer.getvalue(),\n file_name=\"SAP_Migration_Validation_Report.xlsx\",\n mime=\"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet\",\n use_container_width=True,\n type=\"primary\"\n )\n st.caption(\"The report contains 3 sheets: Summary | All Results | Failures Only\")\n\n st.divider()\n\n\n # ── SECTION 9: EMAIL TRIGGER ─────────────────────────────\n # This is the \"Human-in-the-Loop\" gate.\n # The bot NEVER sends email automatically.\n # It always asks the user first — Yes or No.\n\n st.header(\"📧 Step 5: Notify the Remediation Team\")\n\n failures_df = results_df[results_df[\"Status\"] != \"✅ Correct\"]\n\n if failures_df.empty:\n st.success(\"✅ No failures to report — no email needed! Your migration passed all checks.\")\n else:\n st.warning(\n f\"Found **{failed} failures** and **{warnings} warnings** that may need remediation.\"\n )\n st.info(\"Would you like to email a summary to the remediation team?\")\n\n # Place the Yes and No buttons side by side\n yes_col, no_col = st.columns(2)\n\n with yes_col:\n if st.button(\n \"✅ YES — Send Email to Team Now\",\n type=\"primary\",\n use_container_width=True\n ):\n # Check that all email settings are filled in\n if not sender_email:\n st.error(\"❌ Please fill in your Gmail address in the left sidebar.\")\n elif not sender_password:\n st.error(\"❌ Please fill in your Gmail App Password in the left sidebar.\")\n elif not recipient_email:\n st.error(\"❌ Please fill in the remediation team email in the left sidebar.\")\n else:\n # Get AI suggestions if available, or use a placeholder\n ai_suggestions = st.session_state.get(\n \"ai_suggestions\",\n \"AI suggestions not generated yet. \"\n \"Go to the AI Fix Suggestions tab and click Generate to get them, \"\n \"then re-send the email.\"\n )\n\n # Build the full email body\n email_body = f\"\"\"\nSAP DATA MIGRATION VALIDATION REPORT\n=====================================\nGenerated by: SAP Migration Validation Bot\n\nSUMMARY\n-------\nTotal Records Processed : {total}\nCorrectly Migrated : {correct} ({pass_rate}% pass rate)\nFailed Records : {failed}\nWarnings : {warnings}\nKey Column Used : {key_column}\n\nMAPPING RULES VALIDATED\n-----------------------\n{rules_loaded}\n\nRECORDS REQUIRING ATTENTION ({len(failures_df)} records)\n---------------------------------------------------------\n{failures_df.to_string(index=False)}\n\nAI REMEDIATION SUGGESTIONS (Google Gemini)\n------------------------------------------\n{ai_suggestions}\n\n---\nThis report was automatically generated by the SAP Migration Validation Bot.\nPlease do not reply to this email.\n \"\"\"\n\n # Send the email\n with st.spinner(\"📧 Sending email... please wait.\"):\n success, message = send_email(\n sender = sender_email,\n password = sender_password,\n recipient = recipient_email,\n subject = (\n f\"SAP Migration Report — \"\n f\"{failed} Failures | {warnings} Warnings | \"\n f\"{pass_rate}% Pass Rate\"\n ),\n body = email_body\n )\n\n if success:\n st.success(message)\n else:\n st.error(message)\n\n with no_col:\n if st.button(\n \"❌ NO — Skip Email for Now\",\n use_container_width=True\n ):\n st.info(\n \"No problem! Email skipped. \"\n \"You can still download the full Excel report using the button above.\"\n )\n"}}}}