Spaces:
Running
Running
| """AISA Compliance Checker โ HuggingFace Space App. | |
| A web interface for evaluating agentic AI projects against the | |
| AISA (Agentic AI Systems Architecture) reference architecture. | |
| """ | |
| import gradio as gr | |
| import tempfile | |
| import zipfile | |
| import shutil | |
| from pathlib import Path | |
| from datetime import datetime | |
| from aisa.scanner.detector import detect_frameworks | |
| from aisa.scanner.analyzer import scan_project | |
| from aisa.ai_checker.agent import ai_check_project, AICheckConfig | |
| from aisa.ai_checker.combined import combine_results | |
| from aisa.report.generator import generate_markdown_report | |
| from aisa.report.recommendations import get_recommendation, get_priority | |
| from aisa.layers import ALL_LAYERS, ALL_CONTRACTS | |
| # --------------------------------------------------------------------------- | |
| # Core Analysis Functions | |
| # --------------------------------------------------------------------------- | |
| def run_analysis( | |
| zip_file, | |
| analysis_mode: str, | |
| provider: str, | |
| api_key: str, | |
| model_name: str, | |
| progress=gr.Progress(), | |
| ): | |
| """Run AISA compliance analysis on an uploaded project.""" | |
| if zip_file is None: | |
| raise gr.Error("Please upload a ZIP file of your project.") | |
| # Step 1: Extract ZIP | |
| progress(0.05, desc="Extracting project files...") | |
| temp_dir = tempfile.mkdtemp(prefix="aisa_") | |
| project_path = Path(temp_dir) / "project" | |
| project_path.mkdir() | |
| try: | |
| with zipfile.ZipFile(zip_file.name, "r") as zf: | |
| zf.extractall(project_path) | |
| except (zipfile.BadZipFile, Exception) as e: | |
| shutil.rmtree(temp_dir, ignore_errors=True) | |
| raise gr.Error(f"Invalid ZIP file: {e}") | |
| # Check if ZIP has a single root folder | |
| items = list(project_path.iterdir()) | |
| if len(items) == 1 and items[0].is_dir(): | |
| project_path = items[0] | |
| try: | |
| # Step 2: Detect frameworks | |
| progress(0.15, desc="Detecting frameworks...") | |
| frameworks = detect_frameworks(project_path) | |
| # Step 3: Pattern scan | |
| progress(0.25, desc="Running pattern-based scan...") | |
| pattern_result = scan_project(project_path, frameworks) | |
| ai_result = None | |
| # Step 4: AI analysis (if requested) | |
| if analysis_mode in ("AI Analysis", "Full Analysis (Pattern + AI)"): | |
| if not api_key.strip(): | |
| if analysis_mode == "Full Analysis (Pattern + AI)": | |
| progress(0.60, desc="No API key provided, using pattern results only...") | |
| else: | |
| shutil.rmtree(temp_dir, ignore_errors=True) | |
| raise gr.Error("API key is required for AI Analysis. Please enter your API key.") | |
| else: | |
| progress(0.40, desc=f"Running AI analysis with {provider}...") | |
| config = AICheckConfig( | |
| provider=provider.lower(), | |
| model=model_name.strip() or "", | |
| api_key=api_key.strip(), | |
| ) | |
| config = config.resolve() | |
| try: | |
| ai_result = ai_check_project(project_path, config) | |
| except Exception as e: | |
| if analysis_mode == "AI Analysis": | |
| shutil.rmtree(temp_dir, ignore_errors=True) | |
| raise gr.Error(f"AI analysis failed: {e}") | |
| # For full-check, fall back to pattern only | |
| # Step 5: Combine results | |
| progress(0.70, desc="Generating report...") | |
| if analysis_mode == "AI Analysis" and ai_result: | |
| result = ai_result | |
| elif analysis_mode == "Full Analysis (Pattern + AI)" and ai_result: | |
| result = combine_results(pattern_result, ai_result) | |
| else: | |
| result = pattern_result | |
| # Step 6: Generate outputs | |
| progress(0.85, desc="Formatting results...") | |
| # Summary card | |
| summary = _build_summary(result, analysis_mode) | |
| # Layer details | |
| layer_details = _build_layer_details(result) | |
| # Full markdown report | |
| full_report = generate_markdown_report(result) | |
| # Save report to file for download | |
| report_path = Path(temp_dir) / "aisa_report.md" | |
| report_path.write_text(full_report) | |
| progress(1.0, desc="Done!") | |
| return summary, layer_details, full_report, str(report_path) | |
| finally: | |
| # Clean up extracted files but keep report | |
| pass | |
| # --------------------------------------------------------------------------- | |
| # Output Builders | |
| # --------------------------------------------------------------------------- | |
| def _build_summary(result, mode: str) -> str: | |
| """Build the summary overview card.""" | |
| score = result.overall_score | |
| grade = _get_grade(score) | |
| layer_score = result.layer_score | |
| contract_score = result.contract_score | |
| # Framework detection | |
| fw_text = "" | |
| if result.frameworks_detected: | |
| fw_list = sorted(result.frameworks_detected.items(), key=lambda x: -x[1]) | |
| fw_text = " | ".join(f"**{name}** ({conf:.0%})" for name, conf in fw_list[:5]) | |
| else: | |
| fw_text = "No known frameworks detected" | |
| # Stats | |
| files_text = "" | |
| if result.files_scanned > 0: | |
| files_text = f"**Files Scanned:** {result.files_scanned} Python files ({result.total_lines:,} lines)" | |
| # Score bar | |
| filled = int(score / 100 * 30) | |
| bar = "โ" * filled + "โ" * (30 - filled) | |
| # Count gaps | |
| total_criteria = sum(lr.total_count for lr in result.layer_results + result.contract_results) | |
| total_covered = sum(lr.covered_count for lr in result.layer_results + result.contract_results) | |
| gaps = total_criteria - total_covered | |
| return f"""## AISA Compliance Score | |
| # {score:.0f}/100 โ {grade} | |
| `{bar}` | |
| | Metric | Value | | |
| |--------|-------| | |
| | **Analysis Mode** | {mode} | | |
| | **Layer Score** | {layer_score:.0f}% | | |
| | **Contract Score** | {contract_score:.0f}% | | |
| | **Criteria Covered** | {total_covered}/{total_criteria} | | |
| | **Gaps Found** | {gaps} | | |
| | {files_text} | | | |
| ### Detected Frameworks | |
| {fw_text} | |
| """ | |
| def _build_layer_details(result) -> str: | |
| """Build detailed layer-by-layer breakdown.""" | |
| lines = ["## Layer-by-Layer Breakdown\n"] | |
| # Layer table | |
| lines.append("### Architectural Layers\n") | |
| lines.append("| Layer | Coverage | Status | Covered | Gaps |") | |
| lines.append("|-------|:--------:|:------:|:-------:|:----:|") | |
| for lr in result.layer_results: | |
| score = lr.coverage_score | |
| status = _score_label(score) | |
| bar = _mini_bar(score) | |
| lines.append(f"| {lr.id}: {lr.name} | {bar} {score:.0f}% | {status} | {lr.covered_count}/{lr.total_count} | {lr.total_count - lr.covered_count} |") | |
| lines.append("") | |
| # Contract table | |
| lines.append("### Cross-Layer Contracts\n") | |
| lines.append("| Contract | Coverage | Status | Covered | Gaps |") | |
| lines.append("|----------|:--------:|:------:|:-------:|:----:|") | |
| for cr in result.contract_results: | |
| score = cr.coverage_score | |
| status = _score_label(score) | |
| bar = _mini_bar(score) | |
| lines.append(f"| {cr.id}: {cr.name} | {bar} {score:.0f}% | {status} | {cr.covered_count}/{cr.total_count} | {cr.total_count - cr.covered_count} |") | |
| lines.append("") | |
| # Top gaps with recommendations | |
| gaps = [] | |
| for lr in result.layer_results + result.contract_results: | |
| priority = get_priority(lr.id) | |
| for cr in lr.criteria_results: | |
| if not cr.covered: | |
| rec = get_recommendation(cr.criterion_id) or "" | |
| gaps.append((priority, cr.criterion_id, cr.criterion_name, rec)) | |
| priority_order = {"Critical": 0, "High": 1, "Medium": 2} | |
| gaps.sort(key=lambda g: (priority_order.get(g[0], 3), g[1])) | |
| if gaps: | |
| lines.append("### Top Gaps & Recommendations\n") | |
| lines.append("| Priority | Criterion | Gap | Recommendation |") | |
| lines.append("|:--------:|-----------|-----|----------------|") | |
| for priority, cid, name, rec in gaps[:15]: | |
| lines.append(f"| **{priority}** | {cid} | {name} | {rec} |") | |
| if len(gaps) > 15: | |
| lines.append(f"\n*...and {len(gaps) - 15} more gaps. Download the full report for details.*") | |
| return "\n".join(lines) | |
| def _get_grade(score: float) -> str: | |
| if score >= 80: | |
| return "Grade A โ Production Ready" | |
| elif score >= 60: | |
| return "Grade B โ Maturing" | |
| elif score >= 40: | |
| return "Grade C โ Developing" | |
| elif score >= 20: | |
| return "Grade D โ Early Stage" | |
| else: | |
| return "Grade F โ Significant Gaps" | |
| def _score_label(score: float) -> str: | |
| if score >= 80: | |
| return "โ Strong" | |
| elif score >= 50: | |
| return "๐ก Partial" | |
| elif score >= 20: | |
| return "๐ Minimal" | |
| else: | |
| return "๐ด Absent" | |
| def _mini_bar(score: float) -> str: | |
| filled = int(score / 100 * 10) | |
| return "โ" * filled + "โ" * (10 - filled) | |
| # --------------------------------------------------------------------------- | |
| # Gradio UI | |
| # --------------------------------------------------------------------------- | |
| HEADER_MD = """ | |
| # ๐ AISA Compliance Checker | |
| **Evaluate your agentic AI project against the AISA reference architecture.** | |
| Upload your project as a **ZIP file** and get an instant compliance report across | |
| **7 architectural layers** and **4 cross-layer contracts**. | |
| | Layer | What's Checked | | |
| |-------|---------------| | |
| | L1: LLM Foundation | Model adapters, prompts, context management, safety | | |
| | L2: Tool & Environment | Tool schemas, sandboxing, permissions, MCP | | |
| | L3: Cognitive Agent | Planning, memory, goals, reflection | | |
| | L4: Infrastructure | Orchestration, state, multi-agent, tracing | | |
| | L5: Evaluation | Tests, monitoring, regression, human eval | | |
| | L6: Dev & Deployment | Versioning, CI/CD, A/B testing, rollout | | |
| | L7: Governance | Policy-as-code, privacy, fairness, oversight | | |
| **CLI version:** `pip install aisa-checker` | |
| --- | |
| """ | |
| FOOTER_MD = """ | |
| --- | |
| **AISA** (Agentic AI Systems Architecture) โ A unified layered reference architecture | |
| for designing, deploying, evaluating, and governing agentic AI systems. | |
| *Nacar, O., Alquffari, D., & Alkhalifa, M. (2026). Tuwaiq Academy โ TRDC.* | |
| [Paper & Resources](https://huggingface.co/AISA-Framework) | | |
| [PyPI Package](https://pypi.org/project/aisa-checker/) | | |
| [Mapping Study](https://huggingface.co/AISA-Framework) | |
| """ | |
| def create_app(): | |
| """Build the Gradio application.""" | |
| with gr.Blocks( | |
| title="AISA Compliance Checker", | |
| theme=gr.themes.Soft(primary_hue="blue", secondary_hue="purple"), | |
| ) as app: | |
| gr.Markdown(HEADER_MD) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Upload & Configure") | |
| zip_input = gr.File( | |
| label="Upload Project (ZIP)", | |
| file_types=[".zip"], | |
| type="filepath", | |
| ) | |
| analysis_mode = gr.Radio( | |
| choices=[ | |
| "Pattern Scan (Fast)", | |
| "AI Analysis", | |
| "Full Analysis (Pattern + AI)", | |
| ], | |
| value="Pattern Scan (Fast)", | |
| label="Analysis Mode", | |
| info="Pattern scan is free. AI modes require an API key.", | |
| ) | |
| with gr.Accordion("AI Settings (optional)", open=False): | |
| provider = gr.Dropdown( | |
| choices=["openai", "anthropic", "google"], | |
| value="openai", | |
| label="LLM Provider", | |
| ) | |
| api_key = gr.Textbox( | |
| label="API Key", | |
| placeholder="sk-... or sk-ant-... or AIza...", | |
| type="password", | |
| ) | |
| model_name = gr.Textbox( | |
| label="Model (optional, uses default if empty)", | |
| placeholder="e.g., gpt-4o, claude-sonnet-4-20250514, gemini-2.0-flash", | |
| ) | |
| run_btn = gr.Button( | |
| "Run AISA Check", | |
| variant="primary", | |
| size="lg", | |
| ) | |
| gr.Markdown(""" | |
| **How to prepare your ZIP:** | |
| 1. ZIP your project's root directory | |
| 2. Include all Python files, configs, CI/CD files | |
| 3. Exclude `node_modules`, `.venv`, `.git` (they're auto-skipped) | |
| """) | |
| with gr.Column(scale=2): | |
| gr.Markdown("### Results") | |
| with gr.Tabs(): | |
| with gr.Tab("Summary"): | |
| summary_output = gr.Markdown( | |
| value="*Upload a project and click 'Run AISA Check' to see results.*" | |
| ) | |
| with gr.Tab("Layer Details"): | |
| details_output = gr.Markdown( | |
| value="*Results will appear here after analysis.*" | |
| ) | |
| with gr.Tab("Full Report"): | |
| report_output = gr.Markdown( | |
| value="*Full markdown report will appear here.*" | |
| ) | |
| with gr.Tab("Download"): | |
| download_output = gr.File( | |
| label="Download Full Report (Markdown)", | |
| ) | |
| # Wire up the button | |
| run_btn.click( | |
| fn=run_analysis, | |
| inputs=[zip_input, analysis_mode, provider, api_key, model_name], | |
| outputs=[summary_output, details_output, report_output, download_output], | |
| show_progress="full", | |
| ) | |
| gr.Markdown(FOOTER_MD) | |
| return app | |
| # --------------------------------------------------------------------------- | |
| # Launch | |
| # --------------------------------------------------------------------------- | |
| if __name__ == "__main__": | |
| app = create_app() | |
| app.launch() | |