from fastapi import FastAPI, File, UploadFile, Request from fastapi.responses import HTMLResponse, JSONResponse from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware import pandas as pd import io import uvicorn from reconciliation import ReconciliationEngine from anomaly import AnomalyDetector from llm_explainer import LLMExplainer from fraud_graph import FraudGraph from gst_api import GSTGatewayMock import os app = FastAPI() app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Initialize engines try: recon_engine = ReconciliationEngine(threshold=85.0) except Exception as e: recon_engine = None anomaly_detector = AnomalyDetector(contamination=0.05) llm_explainer = LLMExplainer() fraud_graph = FraudGraph() gst_api = GSTGatewayMock() @app.post("/api/reconcile") async def api_reconcile(books: UploadFile = File(...), gst: UploadFile = File(...)): books_content = await books.read() gst_content = await gst.read() try: books_df = pd.read_csv(io.BytesIO(books_content)) gst_df = pd.read_csv(io.BytesIO(gst_content)) except Exception as e: return {"error": "Invalid CSV format. Please ensure you are uploading valid CSV files, not PDFs or Excel documents."} return process_data(books_df, gst_df) @app.post("/api/explain") async def api_explain(request: Request): data = await request.json() row = data.get("row", {}) match_status = data.get("match_status", "Anomaly") b_vendor = data.get("b_vendor", "N/A") g_vendor = data.get("g_vendor", "N/A") b_amount = data.get("b_amount", 0) g_amount = data.get("g_amount", 0) explanation = llm_explainer.explain_discrepancy(row, match_status, b_vendor, g_vendor, b_amount, g_amount) return {"explanation": explanation} @app.post("/api/fetch_live") async def api_fetch_live(books: UploadFile = File(...)): books_content = await books.read() books_df = pd.read_csv(io.BytesIO(books_content)) gst_df = gst_api.fetch_gst_data("2023-01-01", "2023-12-31", "27AADCB2230M1Z2") return process_data(books_df, gst_df) def process_data(books_df, gst_df): if recon_engine is None: return {"error": "Reconciliation engine failed to initialize"} try: merged_df = recon_engine.reconcile(books_df, gst_df) except Exception as e: return {"error": f"Reconciliation failed: {str(e)}"} books_with_anomalies = anomaly_detector.detect_anomalies(books_df, amount_col='Amount') if 'InvoiceID' in merged_df.columns and 'InvoiceID' in books_with_anomalies.columns: merged_df = pd.merge(merged_df, books_with_anomalies[['InvoiceID', 'IsAnomaly', 'AnomalyScore']], on='InvoiceID', how='left') discrepancies = merged_df[merged_df['MatchStatus'] != 'Exact Match'].copy() recon_results = merged_df.fillna("").infer_objects(copy=False).to_dict(orient="records") anomalies = merged_df[merged_df['IsAnomaly'] == True].fillna("").infer_objects(copy=False).to_dict(orient="records") # Compute chart data recon_trend = [0] * 12 discrep_trend = [0] * 12 anomaly_dist = {"critical": [0]*12, "high": [0]*12, "medium": [0]*12} # Try to extract month if InvoiceDate exists date_col = 'InvoiceDate' if 'InvoiceDate' in merged_df.columns else 'InvoiceDate_books' if 'InvoiceDate_books' in merged_df.columns else None if date_col and date_col in merged_df.columns: merged_df['Month'] = pd.to_datetime(merged_df[date_col], errors='coerce').dt.month merged_df['Month'] = merged_df['Month'].fillna(1).astype(int) monthly_recon = merged_df[merged_df['MatchStatus'] == 'Exact Match'].groupby('Month').size() for m, count in monthly_recon.items(): if 1 <= m <= 12: recon_trend[m-1] = int(count) monthly_discrep = merged_df[merged_df['MatchStatus'] != 'Exact Match'].groupby('Month').size() for m, count in monthly_discrep.items(): if 1 <= m <= 12: discrep_trend[m-1] = int(count) monthly_anomalies = merged_df[merged_df['IsAnomaly'] == True] for _, row in monthly_anomalies.iterrows(): m = int(row.get('Month', 1)) if 1 <= m <= 12: score = row.get('AnomalyScore', 0) if score > 0.3: anomaly_dist["critical"][m-1] += 1 elif score > 0.1: anomaly_dist["high"][m-1] += 1 else: anomaly_dist["medium"][m-1] += 1 # Run Fraud Graph Analysis try: fraud_graph.build_graph(merged_df, source_col='VendorName_books', target_col='VendorName_gst', amount_col='Amount_books') cycles = fraud_graph.detect_cycles() risk_scores = fraud_graph.analyze_risk_nodes() fraud_nodes = [{"id": str(n), "label": str(n), "size": 15, "color": "#64748b", "risk_score": risk_scores.get(n, 0.0)} for n in fraud_graph.graph.nodes()] fraud_edges = [{"from": list(fraud_graph.graph.nodes()).index(u), "to": list(fraud_graph.graph.nodes()).index(v), "weight": d.get('weight', 0)} for u, v, d in fraud_graph.graph.edges(data=True)] max_risk = max(risk_scores.values()) if risk_scores else 0.0 overall_risk_score = min(10.0, max_risk * 100) # Arbitrary scale to 0-10 except Exception as e: cycles = [] fraud_nodes = [] fraud_edges = [] overall_risk_score = 0.0 # Get FAISS Stats try: ntotal = recon_engine.index.ntotal if recon_engine and recon_engine.index else 0 mem_mb = round(ntotal * 384 * 4 / (1024 * 1024), 2) except: ntotal = 0 mem_mb = 0 return { "summary": { "total_books": len(books_df), "total_gst": len(gst_df), "exact": len(merged_df[merged_df['MatchStatus'] == 'Exact Match']), "fuzzy": len(merged_df[merged_df['MatchStatus'].str.contains('Fuzzy', na=False)]), "semantic": len(merged_df[merged_df['MatchStatus'].str.contains('Semantic', na=False)]), "discrepancies": len(discrepancies), "unmatched": len(merged_df[merged_df['MatchStatus'].str.contains('Mismatch', na=False) | merged_df['MatchStatus'].str.contains('Missing', na=False)]), "anomalies": len(anomalies), "fraud_rings": len(cycles), "overall_risk_score": overall_risk_score }, "charts": { "recon_trend": recon_trend, "discrep_trend": discrep_trend, "anomaly_dist": anomaly_dist }, "fraud_network": { "nodes": fraud_nodes, "edges": fraud_edges, "cycles": cycles }, "faiss_stats": { "ntotal": ntotal, "memory_mb": mem_mb }, "reconciliation": recon_results[:50], # Limit payload for UI "anomalies": anomalies[:50] } # Serve the frontend files app.mount("/", StaticFiles(directory=".", html=True), name="static") if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=7860)