Commit
ff3fa15
Β·
verified Β·
1 Parent(s): 2ea60aa

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +405 -0
app.py ADDED
@@ -0,0 +1,405 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """AISA Compliance Checker β€” HuggingFace Space App.
2
+
3
+ A web interface for evaluating agentic AI projects against the
4
+ AISA (Agentic AI Systems Architecture) reference architecture.
5
+ """
6
+
7
+ import gradio as gr
8
+ import tempfile
9
+ import zipfile
10
+ import shutil
11
+ from pathlib import Path
12
+ from datetime import datetime
13
+
14
+ from aisa.scanner.detector import detect_frameworks
15
+ from aisa.scanner.analyzer import scan_project
16
+ from aisa.ai_checker.agent import ai_check_project, AICheckConfig
17
+ from aisa.ai_checker.combined import combine_results
18
+ from aisa.report.generator import generate_markdown_report
19
+ from aisa.report.recommendations import get_recommendation, get_priority
20
+ from aisa.layers import ALL_LAYERS, ALL_CONTRACTS
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Core Analysis Functions
25
+ # ---------------------------------------------------------------------------
26
+
27
+ def run_analysis(
28
+ zip_file,
29
+ analysis_mode: str,
30
+ provider: str,
31
+ api_key: str,
32
+ model_name: str,
33
+ progress=gr.Progress(),
34
+ ):
35
+ """Run AISA compliance analysis on an uploaded project."""
36
+ if zip_file is None:
37
+ raise gr.Error("Please upload a ZIP file of your project.")
38
+
39
+ # Step 1: Extract ZIP
40
+ progress(0.05, desc="Extracting project files...")
41
+ temp_dir = tempfile.mkdtemp(prefix="aisa_")
42
+ project_path = Path(temp_dir) / "project"
43
+ project_path.mkdir()
44
+
45
+ try:
46
+ with zipfile.ZipFile(zip_file.name, "r") as zf:
47
+ zf.extractall(project_path)
48
+ except (zipfile.BadZipFile, Exception) as e:
49
+ shutil.rmtree(temp_dir, ignore_errors=True)
50
+ raise gr.Error(f"Invalid ZIP file: {e}")
51
+
52
+ # Check if ZIP has a single root folder
53
+ items = list(project_path.iterdir())
54
+ if len(items) == 1 and items[0].is_dir():
55
+ project_path = items[0]
56
+
57
+ try:
58
+ # Step 2: Detect frameworks
59
+ progress(0.15, desc="Detecting frameworks...")
60
+ frameworks = detect_frameworks(project_path)
61
+
62
+ # Step 3: Pattern scan
63
+ progress(0.25, desc="Running pattern-based scan...")
64
+ pattern_result = scan_project(project_path, frameworks)
65
+
66
+ ai_result = None
67
+
68
+ # Step 4: AI analysis (if requested)
69
+ if analysis_mode in ("AI Analysis", "Full Analysis (Pattern + AI)"):
70
+ if not api_key.strip():
71
+ if analysis_mode == "Full Analysis (Pattern + AI)":
72
+ progress(0.60, desc="No API key provided, using pattern results only...")
73
+ else:
74
+ shutil.rmtree(temp_dir, ignore_errors=True)
75
+ raise gr.Error("API key is required for AI Analysis. Please enter your API key.")
76
+ else:
77
+ progress(0.40, desc=f"Running AI analysis with {provider}...")
78
+ config = AICheckConfig(
79
+ provider=provider.lower(),
80
+ model=model_name.strip() or "",
81
+ api_key=api_key.strip(),
82
+ )
83
+ config = config.resolve()
84
+ try:
85
+ ai_result = ai_check_project(project_path, config)
86
+ except Exception as e:
87
+ if analysis_mode == "AI Analysis":
88
+ shutil.rmtree(temp_dir, ignore_errors=True)
89
+ raise gr.Error(f"AI analysis failed: {e}")
90
+ # For full-check, fall back to pattern only
91
+
92
+ # Step 5: Combine results
93
+ progress(0.70, desc="Generating report...")
94
+ if analysis_mode == "AI Analysis" and ai_result:
95
+ result = ai_result
96
+ elif analysis_mode == "Full Analysis (Pattern + AI)" and ai_result:
97
+ result = combine_results(pattern_result, ai_result)
98
+ else:
99
+ result = pattern_result
100
+
101
+ # Step 6: Generate outputs
102
+ progress(0.85, desc="Formatting results...")
103
+
104
+ # Summary card
105
+ summary = _build_summary(result, analysis_mode)
106
+
107
+ # Layer details
108
+ layer_details = _build_layer_details(result)
109
+
110
+ # Full markdown report
111
+ full_report = generate_markdown_report(result)
112
+
113
+ # Save report to file for download
114
+ report_path = Path(temp_dir) / "aisa_report.md"
115
+ report_path.write_text(full_report)
116
+
117
+ progress(1.0, desc="Done!")
118
+
119
+ return summary, layer_details, full_report, str(report_path)
120
+
121
+ finally:
122
+ # Clean up extracted files but keep report
123
+ pass
124
+
125
+
126
+ # ---------------------------------------------------------------------------
127
+ # Output Builders
128
+ # ---------------------------------------------------------------------------
129
+
130
+ def _build_summary(result, mode: str) -> str:
131
+ """Build the summary overview card."""
132
+ score = result.overall_score
133
+ grade = _get_grade(score)
134
+ layer_score = result.layer_score
135
+ contract_score = result.contract_score
136
+
137
+ # Framework detection
138
+ fw_text = ""
139
+ if result.frameworks_detected:
140
+ fw_list = sorted(result.frameworks_detected.items(), key=lambda x: -x[1])
141
+ fw_text = " | ".join(f"**{name}** ({conf:.0%})" for name, conf in fw_list[:5])
142
+ else:
143
+ fw_text = "No known frameworks detected"
144
+
145
+ # Stats
146
+ files_text = ""
147
+ if result.files_scanned > 0:
148
+ files_text = f"**Files Scanned:** {result.files_scanned} Python files ({result.total_lines:,} lines)"
149
+
150
+ # Score bar
151
+ filled = int(score / 100 * 30)
152
+ bar = "β–ˆ" * filled + "β–‘" * (30 - filled)
153
+
154
+ # Count gaps
155
+ total_criteria = sum(lr.total_count for lr in result.layer_results + result.contract_results)
156
+ total_covered = sum(lr.covered_count for lr in result.layer_results + result.contract_results)
157
+ gaps = total_criteria - total_covered
158
+
159
+ return f"""## AISA Compliance Score
160
+
161
+ # {score:.0f}/100 β€” {grade}
162
+
163
+ `{bar}`
164
+
165
+ | Metric | Value |
166
+ |--------|-------|
167
+ | **Analysis Mode** | {mode} |
168
+ | **Layer Score** | {layer_score:.0f}% |
169
+ | **Contract Score** | {contract_score:.0f}% |
170
+ | **Criteria Covered** | {total_covered}/{total_criteria} |
171
+ | **Gaps Found** | {gaps} |
172
+ | {files_text} | |
173
+
174
+ ### Detected Frameworks
175
+ {fw_text}
176
+ """
177
+
178
+
179
+ def _build_layer_details(result) -> str:
180
+ """Build detailed layer-by-layer breakdown."""
181
+ lines = ["## Layer-by-Layer Breakdown\n"]
182
+
183
+ # Layer table
184
+ lines.append("### Architectural Layers\n")
185
+ lines.append("| Layer | Coverage | Status | Covered | Gaps |")
186
+ lines.append("|-------|:--------:|:------:|:-------:|:----:|")
187
+
188
+ for lr in result.layer_results:
189
+ score = lr.coverage_score
190
+ status = _score_label(score)
191
+ bar = _mini_bar(score)
192
+ lines.append(f"| {lr.id}: {lr.name} | {bar} {score:.0f}% | {status} | {lr.covered_count}/{lr.total_count} | {lr.total_count - lr.covered_count} |")
193
+
194
+ lines.append("")
195
+
196
+ # Contract table
197
+ lines.append("### Cross-Layer Contracts\n")
198
+ lines.append("| Contract | Coverage | Status | Covered | Gaps |")
199
+ lines.append("|----------|:--------:|:------:|:-------:|:----:|")
200
+
201
+ for cr in result.contract_results:
202
+ score = cr.coverage_score
203
+ status = _score_label(score)
204
+ bar = _mini_bar(score)
205
+ lines.append(f"| {cr.id}: {cr.name} | {bar} {score:.0f}% | {status} | {cr.covered_count}/{cr.total_count} | {cr.total_count - cr.covered_count} |")
206
+
207
+ lines.append("")
208
+
209
+ # Top gaps with recommendations
210
+ gaps = []
211
+ for lr in result.layer_results + result.contract_results:
212
+ priority = get_priority(lr.id)
213
+ for cr in lr.criteria_results:
214
+ if not cr.covered:
215
+ rec = get_recommendation(cr.criterion_id) or ""
216
+ gaps.append((priority, cr.criterion_id, cr.criterion_name, rec))
217
+
218
+ priority_order = {"Critical": 0, "High": 1, "Medium": 2}
219
+ gaps.sort(key=lambda g: (priority_order.get(g[0], 3), g[1]))
220
+
221
+ if gaps:
222
+ lines.append("### Top Gaps & Recommendations\n")
223
+ lines.append("| Priority | Criterion | Gap | Recommendation |")
224
+ lines.append("|:--------:|-----------|-----|----------------|")
225
+ for priority, cid, name, rec in gaps[:15]:
226
+ lines.append(f"| **{priority}** | {cid} | {name} | {rec} |")
227
+ if len(gaps) > 15:
228
+ lines.append(f"\n*...and {len(gaps) - 15} more gaps. Download the full report for details.*")
229
+
230
+ return "\n".join(lines)
231
+
232
+
233
+ def _get_grade(score: float) -> str:
234
+ if score >= 80:
235
+ return "Grade A β€” Production Ready"
236
+ elif score >= 60:
237
+ return "Grade B β€” Maturing"
238
+ elif score >= 40:
239
+ return "Grade C β€” Developing"
240
+ elif score >= 20:
241
+ return "Grade D β€” Early Stage"
242
+ else:
243
+ return "Grade F β€” Significant Gaps"
244
+
245
+
246
+ def _score_label(score: float) -> str:
247
+ if score >= 80:
248
+ return "βœ… Strong"
249
+ elif score >= 50:
250
+ return "🟑 Partial"
251
+ elif score >= 20:
252
+ return "🟠 Minimal"
253
+ else:
254
+ return "πŸ”΄ Absent"
255
+
256
+
257
+ def _mini_bar(score: float) -> str:
258
+ filled = int(score / 100 * 10)
259
+ return "β–ˆ" * filled + "β–‘" * (10 - filled)
260
+
261
+
262
+ # ---------------------------------------------------------------------------
263
+ # Gradio UI
264
+ # ---------------------------------------------------------------------------
265
+
266
+ HEADER_MD = """
267
+ # πŸ” AISA Compliance Checker
268
+
269
+ **Evaluate your agentic AI project against the AISA reference architecture.**
270
+
271
+ Upload your project as a **ZIP file** and get an instant compliance report across
272
+ **7 architectural layers** and **4 cross-layer contracts**.
273
+
274
+ | Layer | What's Checked |
275
+ |-------|---------------|
276
+ | L1: LLM Foundation | Model adapters, prompts, context management, safety |
277
+ | L2: Tool & Environment | Tool schemas, sandboxing, permissions, MCP |
278
+ | L3: Cognitive Agent | Planning, memory, goals, reflection |
279
+ | L4: Infrastructure | Orchestration, state, multi-agent, tracing |
280
+ | L5: Evaluation | Tests, monitoring, regression, human eval |
281
+ | L6: Dev & Deployment | Versioning, CI/CD, A/B testing, rollout |
282
+ | L7: Governance | Policy-as-code, privacy, fairness, oversight |
283
+
284
+ **CLI version:** `pip install aisa-checker`
285
+
286
+ ---
287
+ """
288
+
289
+ FOOTER_MD = """
290
+ ---
291
+
292
+ **AISA** (Agentic AI Systems Architecture) β€” A unified layered reference architecture
293
+ for designing, deploying, evaluating, and governing agentic AI systems.
294
+
295
+ *Nacar, O., Alquffari, D., & Alkhalifa, M. (2026). Tuwaiq Academy β€” TRDC.*
296
+
297
+ [Paper & Resources](https://huggingface.co/AISA-Framework) |
298
+ [PyPI Package](https://pypi.org/project/aisa-checker/) |
299
+ [Mapping Study](https://huggingface.co/AISA-Framework)
300
+ """
301
+
302
+
303
+ def create_app():
304
+ """Build the Gradio application."""
305
+ with gr.Blocks(
306
+ title="AISA Compliance Checker",
307
+ theme=gr.themes.Soft(primary_hue="blue", secondary_hue="purple"),
308
+ ) as app:
309
+
310
+ gr.Markdown(HEADER_MD)
311
+
312
+ with gr.Row():
313
+ with gr.Column(scale=1):
314
+ gr.Markdown("### Upload & Configure")
315
+
316
+ zip_input = gr.File(
317
+ label="Upload Project (ZIP)",
318
+ file_types=[".zip"],
319
+ type="filepath",
320
+ )
321
+
322
+ analysis_mode = gr.Radio(
323
+ choices=[
324
+ "Pattern Scan (Fast)",
325
+ "AI Analysis",
326
+ "Full Analysis (Pattern + AI)",
327
+ ],
328
+ value="Pattern Scan (Fast)",
329
+ label="Analysis Mode",
330
+ info="Pattern scan is free. AI modes require an API key.",
331
+ )
332
+
333
+ with gr.Accordion("AI Settings (optional)", open=False):
334
+ provider = gr.Dropdown(
335
+ choices=["openai", "anthropic", "google"],
336
+ value="openai",
337
+ label="LLM Provider",
338
+ )
339
+ api_key = gr.Textbox(
340
+ label="API Key",
341
+ placeholder="sk-... or sk-ant-... or AIza...",
342
+ type="password",
343
+ )
344
+ model_name = gr.Textbox(
345
+ label="Model (optional, uses default if empty)",
346
+ placeholder="e.g., gpt-4o, claude-sonnet-4-20250514, gemini-2.0-flash",
347
+ )
348
+
349
+ run_btn = gr.Button(
350
+ "Run AISA Check",
351
+ variant="primary",
352
+ size="lg",
353
+ )
354
+
355
+ gr.Markdown("""
356
+ **How to prepare your ZIP:**
357
+ 1. ZIP your project's root directory
358
+ 2. Include all Python files, configs, CI/CD files
359
+ 3. Exclude `node_modules`, `.venv`, `.git` (they're auto-skipped)
360
+ """)
361
+
362
+ with gr.Column(scale=2):
363
+ gr.Markdown("### Results")
364
+
365
+ with gr.Tabs():
366
+ with gr.Tab("Summary"):
367
+ summary_output = gr.Markdown(
368
+ value="*Upload a project and click 'Run AISA Check' to see results.*"
369
+ )
370
+
371
+ with gr.Tab("Layer Details"):
372
+ details_output = gr.Markdown(
373
+ value="*Results will appear here after analysis.*"
374
+ )
375
+
376
+ with gr.Tab("Full Report"):
377
+ report_output = gr.Markdown(
378
+ value="*Full markdown report will appear here.*"
379
+ )
380
+
381
+ with gr.Tab("Download"):
382
+ download_output = gr.File(
383
+ label="Download Full Report (Markdown)",
384
+ )
385
+
386
+ # Wire up the button
387
+ run_btn.click(
388
+ fn=run_analysis,
389
+ inputs=[zip_input, analysis_mode, provider, api_key, model_name],
390
+ outputs=[summary_output, details_output, report_output, download_output],
391
+ show_progress="full",
392
+ )
393
+
394
+ gr.Markdown(FOOTER_MD)
395
+
396
+ return app
397
+
398
+
399
+ # ---------------------------------------------------------------------------
400
+ # Launch
401
+ # ---------------------------------------------------------------------------
402
+
403
+ if __name__ == "__main__":
404
+ app = create_app()
405
+ app.launch()