rt0209/S4HANA_Migration_Intelligence_Agent
0
1"""2S/4HANA Migration Intelligence Agent — HuggingFace Spaces (Gradio)3===================================================================4Entry point: app.py5SDK: gradio6 7Set in HF Space Secrets:8 ZAI_API_KEY → your key from https://z.ai (or Together AI / OpenRouter)9 ZAI_BASE_URL → https://api.z.ai/v1 (optional, this is the default)10 GLM_MODEL → glm-5.2 (optional, this is the default)11"""12 13import sys, os14#sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))15os.environ["NO_PROXY"] = "localhost,127.0.0.1,0.0.0.0"16os.environ["no_proxy"] = "localhost,127.0.0.1,0.0.0.0"17os.environ["GRADIO_ANALYTICS_ENABLED"] = "False"18import gradio as gr19import asyncio20import os21from pathlib import Path22from dotenv import load_dotenv23 24load_dotenv()25 26from abap_analyzer import ABAPAnalyzer27from report_generator import ReportGenerator28 29# ── Sample ABAP code ──────────────────────────────────────────────────30 31SAMPLES = {32 "Vendor Aging Report": {33 "name": "ZVENDOR_AGING_REPORT",34 "type": "ABAP Report",35 "code": """\36REPORT ZVENDOR_AGING_REPORT.37INCLUDE: <ICON>.38 39* Deprecated: Logical database removed in S/4HANA40NODES: LFB1.41TABLES: BSEG, LFA1, LFB1.42 43SELECT-OPTIONS: S_BUKRS FOR BSEG-BUKRS.44SELECT-OPTIONS: S_GJAHR FOR BSEG-GJAHR.45 46CONSTANTS: gc_bukrs TYPE BUKRS VALUE '1000'. " Hard-coded company code47 48START-OF-SELECTION.49 " CRITICAL: BSEG removed in S/4HANA — replaced by ACDOCA50 SELECT * FROM BSEG51 WHERE BUKRS IN S_BUKRS52 AND GJAHR IN S_GJAHR53 AND KOART = 'K'54 INTO TABLE @DATA(lt_bseg).55 56 LOOP AT lt_bseg INTO DATA(ls_bseg).57 " Performance: SELECT SINGLE inside LOOP58 SELECT SINGLE NAME1 FROM LFA159 WHERE LIFNR = ls_bseg-LIFNR60 INTO @DATA(lv_name).61 ENDLOOP.62 63 CALL FUNCTION 'REUSE_ALV_GRID_DISPLAY'64 EXPORTING i_structure_name = 'BSEG'65 TABLES t_outtab = lt_bseg.\66""",67 },68 "Material Stock Check": {69 "name": "ZMAT_STOCK_CHECK",70 "type": "ABAP Report",71 "code": """\72REPORT ZMAT_STOCK_CHECK.73 74DATA: lv_matnr TYPE MATNR. " 18-char field — S/4HANA extends to 4075 76" CRITICAL: MARD table retired in S/4HANA77SELECT MATNR WERKS LABST EINME78 FROM MARD79 WHERE WERKS = '0001'80 AND MATNR = @lv_matnr81 INTO TABLE @DATA(lt_stock).82 83" Integration risk: RFC to external BW system84CALL FUNCTION 'ZBAPI_GET_FORECAST'85 DESTINATION 'BW_SYSTEM'86 EXPORTING matnr = lv_matnr87 IMPORTING forecast = DATA(ls_forecast).88 89" Old user exit style — must migrate to BAdI90CALL CUSTOMER-FUNCTION '001'91 EXPORTING matnr = lv_matnr.92 93CONSTANTS: gc_werks TYPE WERKS_D VALUE '0001'. " Hard-coded plant\94""",95 },96 "GL Posting Function": {97 "name": "ZFI_POST_GL_DOCUMENT",98 "type": "Function Module",99 "code": """\100FUNCTION ZFI_POST_GL_DOCUMENT.101*" Importing: BUKRS TYPE BUKRS, BLDAT TYPE BLDAT102*" Exporting: BELNR TYPE BELNR_D103 104 DATA: ls_header TYPE BAPIACHE09,105 lt_gl TYPE TABLE OF BAPIACGL09,106 lt_curr TYPE TABLE OF BAPIACCR09.107 108 " Deprecated BAPI — use BAPI_ACCFI_POST in S/4HANA109 CALL FUNCTION 'BAPI_ACC_DOCUMENT_POST'110 EXPORTING documentheader = ls_header111 TABLES accountgl = lt_gl112 currencyamount = lt_curr.113 114 " CRITICAL: Direct DB modify — data corruption risk in S/4HANA115 MODIFY BKPF FROM ls_bkpf.116 MODIFY BSEG FROM ls_bseg.117 118 " Hard-coded chart of accounts119 DATA: lv_ktopl TYPE KTOPL VALUE 'INT'.120 121ENDFUNCTION.\122""",123 },124}125 126OBJECT_TYPES = [127 "ABAP Report", "Function Module", "Function Group",128 "ABAP Class", "ABAP Interface", "BAdI Implementation",129 "SmartForm", "SAP Script", "Transformation",130 "User Exit", "Custom Table", "Other",131]132 133# ── Severity palette ──────────────────────────────────────────────────134 135_SEV = {136 "Critical": ("#FCEBEB", "#A32D2D", "#F09595"),137 "High": ("#FAEEDA", "#854F0B", "#EF9F27"),138 "Medium": ("#E6F1FB", "#0C447C", "#85B7EB"),139 "Low": ("#EAF3DE", "#27500A", "#97C459"),140}141 142def _badge(severity: str) -> str:143 bg, fg, bd = _SEV.get(severity, ("#f5f5f5", "#666", "#ccc"))144 return (145 f'<span style="background:{bg};color:{fg};border:0.5px solid {bd};'146 f'padding:2px 10px;border-radius:20px;font-size:11px;font-weight:600">'147 f'{severity}</span>'148 )149 150def _metric(label: str, value: str) -> str:151 return (152 f'<div style="background:#f8f9fa;border-radius:8px;padding:12px 14px">'153 f'<div style="font-size:10px;font-weight:600;color:#888;text-transform:uppercase;'154 f'letter-spacing:0.05em;margin-bottom:4px">{label}</div>'155 f'<div style="font-size:20px;font-weight:600;color:#1a1a1a">{value}</div>'156 f'</div>'157 )158 159# ── HTML formatter ────────────────────────────────────────────────────160 161def format_results(result) -> str:162 """Convert an AnalysisResult into styled HTML for gr.HTML display."""163 164 r = result.model_dump() if hasattr(result, "model_dump") else result165 166 sev = r.get("overall_risk", "Medium")167 bg, fg, bd = _SEV.get(sev, ("#f5f5f5", "#666", "#ccc"))168 169 findings = r.get("findings", [])170 counts = {s: 0 for s in _SEV}171 for f in findings:172 counts[f.get("severity", "Medium")] = counts.get(f.get("severity", "Medium"), 0) + 1173 174 severity_order = ["Critical", "High", "Medium", "Low"]175 sorted_findings = sorted(176 findings,177 key=lambda f: severity_order.index(f.get("severity", "Low"))178 if f.get("severity") in severity_order else 99,179 )180 181 sev_pills = " ".join(182 _badge(f"{c} {s}")183 for s, c in counts.items() if c > 0184 )185 186 finding_html = ""187 for f in sorted_findings:188 s = f.get("severity", "Medium")189 fbg, ffg, fbd = _SEV.get(s, ("#f5f5f5", "#666", "#ccc"))190 finding_html += f"""191<details style="margin-bottom:8px;border-left:3px solid {ffg};background:#fafafa;192 border-radius:0 6px 6px 0;overflow:hidden">193 <summary style="padding:11px 14px;cursor:pointer;display:flex;align-items:center;194 gap:8px;list-style:none;-webkit-appearance:none">195 {_badge(s)}196 <span style="font-weight:500;color:#1a1a1a;font-size:13px">{f.get('title','')}</span>197 <span style="color:#aaa;font-size:11px;margin-left:auto;white-space:nowrap">198 {f.get('category','')} · {f.get('effort_days', 0)}d199 </span>200 </summary>201 <div style="padding:0 14px 14px;border-top:0.5px solid #eee">202 <table style="width:100%;border-collapse:collapse;margin-top:12px;font-size:13px">203 <tr>204 <td style="width:130px;color:#888;padding:4px 0;vertical-align:top">Location</td>205 <td style="color:#333;padding:4px 0;font-family:monospace">{f.get('line_reference','')}</td>206 </tr>207 <tr>208 <td style="color:#888;padding:4px 0;vertical-align:top">Issue</td>209 <td style="color:#333;padding:4px 0">{f.get('issue','')}</td>210 </tr>211 <tr>212 <td style="color:#888;padding:4px 0;vertical-align:top">S/4 impact</td>213 <td style="color:#333;padding:4px 0">{f.get('impact','')}</td>214 </tr>215 <tr>216 <td style="color:#888;padding:4px 0;vertical-align:top">Remediation</td>217 <td style="color:#0C447C;padding:4px 0;font-weight:500">{f.get('remediation','')}</td>218 </tr>219 </table>220 </div>221</details>222"""223 224 recs = r.get("recommendations", [])225 rec_html = "".join(226 f'<li style="margin-bottom:8px;color:#444;line-height:1.5">{rec}</li>'227 for rec in recs228 )229 230 return f"""231<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;232 line-height:1.5;max-width:100%;padding-bottom:8px">233 234 <!-- Header -->235 <div style="display:flex;align-items:center;gap:10px;margin-bottom:16px;flex-wrap:wrap">236 <h2 style="margin:0;font-size:17px;color:#1a1a1a">{r.get('object_name','')}</h2>237 <span style="background:{bg};color:{fg};border:0.5px solid {bd};238 padding:3px 12px;border-radius:20px;font-size:12px;font-weight:600">239 {sev} Risk240 </span>241 <span style="color:#888;font-size:12px">{r.get('object_type','')}</span>242 </div>243 244 <!-- Metrics -->245 <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:16px">246 {_metric("Readiness score", f"{r.get('s4_readiness_score', 0)}/100")}247 {_metric("Effort estimate", f"{r.get('estimated_effort_days', 0)}d")}248 {_metric("Findings", str(len(findings)))}249 {_metric("Complexity", r.get('migration_complexity', '-'))}250 </div>251 252 <!-- Severity pills -->253 <div style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap">{sev_pills}</div>254 255 <!-- Executive summary -->256 <div style="background:#f8f9fa;border-radius:8px;padding:14px;margin-bottom:18px;257 color:#555;font-size:13px;line-height:1.65">258 {r.get('executive_summary', '')}259 </div>260 261 <!-- Findings -->262 <h3 style="font-size:14px;font-weight:600;color:#1a1a1a;margin:0 0 10px">263 Findings ({len(findings)})264 </h3>265 {finding_html if finding_html else '<p style="color:#888;font-size:13px">No findings.</p>'}266 267 <!-- Recommendations -->268 <h3 style="font-size:14px;font-weight:600;color:#1a1a1a;margin:18px 0 10px">269 Recommendations270 </h3>271 <ol style="padding-left:18px;margin:0">{rec_html}</ol>272 273</div>274"""275 276# ── Analysis handler ──────────────────────────────────────────────────277 278async def run_analysis(279 code: str,280 object_name: str,281 object_type: str,282 client_name: str,283 progress=gr.Progress(),284) -> tuple[str, str | None]:285 """286 Called by Gradio on button click.287 Returns (html_string, docx_path_or_None).288 """289 if not code or not code.strip():290 return "<p style='color:#c00'>Please paste ABAP code or load a sample first.</p>", None291 292 api_key = os.getenv("ZAI_API_KEY", "")293 if not api_key:294 return (295 "<p style='color:#c00'>"296 "<strong>ZAI_API_KEY not set.</strong><br>"297 "Add it in Space Settings → Secrets → New Secret: <code>ZAI_API_KEY</code>. "298 "Get a key at <a href='https://z.ai' target='_blank'>z.ai</a>."299 "</p>",300 None,301 )302 303 try:304 progress(0.05, desc="Initialising GLM-5.2 client...")305 analyzer = ABAPAnalyzer()306 307 def _cb(pct: int, msg: str):308 progress(min(pct / 100, 0.92), desc=msg)309 310 progress(0.10, desc="Sending to GLM-5.2 (1M context)...")311 result = await analyzer.analyze(312 source_code=code,313 object_name=object_name or "ZCUSTOM_OBJECT",314 object_type=object_type or "ABAP Object",315 progress_callback=_cb,316 )317 318 progress(0.94, desc="Generating DOCX report...")319 gen = ReportGenerator()320 report_path = gen.generate_docx(321 results=[result],322 job_id=object_name.replace("/", "_"),323 client_name=client_name or "Client",324 project_name="S/4HANA Migration Assessment",325 )326 327 progress(1.0, desc="Done!")328 html = format_results(result)329 return html, str(report_path)330 331 except Exception as exc:332 return (333 f"<p style='color:#c00'><strong>Analysis failed:</strong> {exc}</p>",334 None,335 )336 337 338# ── Sample loader ─────────────────────────────────────────────────────339 340def load_sample(name: str):341 s = SAMPLES.get(name, {})342 return s.get("code", ""), s.get("name", "ZCUSTOM_OBJECT"), s.get("type", "ABAP Report")343 344 345# ── Gradio UI ─────────────────────────────────────────────────────────346 347DESCRIPTION = """348### 🏭 S/4HANA Migration Intelligence Agent349Powered by **GLM-5.2** (1M token context · open-weight MIT license)350 351Paste ABAP source code, click **Analyse**, and get a structured risk assessment:352deprecated API detection, removed table flags, effort estimate, and a downloadable DOCX report.353 354> Built by **Rohan_Tripathi** as a migration accelerator for SAP clients.355"""356 357with gr.Blocks(358 theme=gr.themes.Soft(primary_hue="blue", neutral_hue="slate"),359 title="S/4HANA Migration Intelligence Agent",360) as demo:361 362 gr.Markdown(DESCRIPTION)363 364 with gr.Row():365 # ── Left column: inputs ──────────────────────────────────────366 with gr.Column(scale=1, min_width=320):367 gr.Markdown("#### Input")368 369 sample_radio = gr.Radio(370 choices=list(SAMPLES.keys()),371 label="Load a sample",372 value=None,373 )374 375 with gr.Row():376 object_name = gr.Textbox(377 value="ZCUSTOM_OBJECT",378 label="Object name",379 placeholder="ZREPORT_CUSTOM",380 scale=2,381 )382 object_type = gr.Dropdown(383 choices=OBJECT_TYPES,384 value="ABAP Report",385 label="Type",386 scale=1,387 )388 389 client_name = gr.Textbox(390 value="Client",391 label="Client name (for DOCX report header)",392 393 )394 395 code_input = gr.Code(396 label="ABAP source code",397 language=None,398 lines=24,399 )400 401 analyze_btn = gr.Button(402 "🔍 Analyse for S/4HANA migration",403 variant="primary",404 size="lg",405 )406 407 # ── Right column: results ─────────────────────────────────────408 with gr.Column(scale=1, min_width=360):409 gr.Markdown("#### Analysis Results")410 411 results_display = gr.HTML(412 value=(413 "<div style='padding:40px;text-align:center;color:#aaa;font-size:14px'>"414 "Analysis results will appear here.</div>"415 )416 )417 418 report_file = gr.File(419 label="📄 Download Migration Report (DOCX)",420 visible=True,421 file_types=[".docx"],422 )423 424 # ── Event wiring ──────────────────────────────────────────────────425 426 sample_radio.change(427 fn=load_sample,428 inputs=[sample_radio],429 outputs=[code_input, object_name, object_type],430 )431 432 analyze_btn.click(433 fn=run_analysis,434 inputs=[code_input, object_name, object_type, client_name],435 outputs=[results_display, report_file],436 )437 438 gr.Markdown(439 "<div style='text-align:center;color:#aaa;font-size:12px;margin-top:12px'>"440 "Set <code>ZAI_API_KEY</code> in Space Secrets → "441 "<a href='https://z.ai' target='_blank'>get a key at z.ai</a>"442 "</div>"443 )444 445 446if __name__ == "__main__":447 demo.queue().launch(448 server_name = "0.0.0.0",449 server_port = 7860,450 share = True,451 )