Pranoy Mukherjee commited on
Commit
3bc3287
·
1 Parent(s): 1c77a4c

Add Gradio UI polish and Spaces entrypoint

Browse files
Files changed (4) hide show
  1. README.md +19 -0
  2. app.py +8 -0
  3. app/ui/gradio_app.py +57 -17
  4. tests/test_gradio_app.py +45 -0
README.md CHANGED
@@ -24,6 +24,12 @@ Run the FastAPI backend:
24
  uvicorn app.main:app --reload
25
  ```
26
 
 
 
 
 
 
 
27
  Health check:
28
 
29
  ```bash
@@ -36,6 +42,15 @@ Run the Gradio demo:
36
  python -m app.ui.gradio_app
37
  ```
38
 
 
 
 
 
 
 
 
 
 
39
  ## Configuration
40
 
41
  Copy `.env.example` to `.env` for local overrides. Default inference mode is:
@@ -68,6 +83,10 @@ Reports preserve full finding totals while displaying a prioritized subset for r
68
  - Docs Agent: flags incomplete README guidance and public Python symbols missing docstrings.
69
  - Synthesizer Agent: deduplicates findings, sorts by severity, and builds the final report.
70
 
 
 
 
 
71
  ## Tests
72
 
73
  ```bash
 
24
  uvicorn app.main:app --reload
25
  ```
26
 
27
+ If port 8000 is busy on Windows, use:
28
+
29
+ ```bash
30
+ uvicorn app.main:app --reload --port 8001
31
+ ```
32
+
33
  Health check:
34
 
35
  ```bash
 
42
  python -m app.ui.gradio_app
43
  ```
44
 
45
+ For Hugging Face Spaces-style startup:
46
+
47
+ ```bash
48
+ python app.py
49
+ ```
50
+
51
+ The Gradio app includes example repos, a live agent progress panel, and a structured markdown report panel.
52
+ The launcher binds to `0.0.0.0` and uses `PORT` when provided, which matches hosted Gradio deployment expectations.
53
+
54
  ## Configuration
55
 
56
  Copy `.env.example` to `.env` for local overrides. Default inference mode is:
 
83
  - Docs Agent: flags incomplete README guidance and public Python symbols missing docstrings.
84
  - Synthesizer Agent: deduplicates findings, sorts by severity, and builds the final report.
85
 
86
+ ## Hugging Face Spaces
87
+
88
+ SwarmAudit is ready to launch as a Gradio Space with the root `app.py` entrypoint. Keep `LLM_PROVIDER=mock` for a reliable public demo, then switch to `LLM_PROVIDER=vllm` when an AMD MI300X-hosted Qwen2.5-Coder endpoint is available.
89
+
90
  ## Tests
91
 
92
  ```bash
app.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ from app.ui.gradio_app import build_app, launch_app
2
+
3
+
4
+ demo = build_app()
5
+
6
+
7
+ if __name__ == "__main__":
8
+ launch_app()
app/ui/gradio_app.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import gradio as gr
2
 
3
  from app.agents.graph import AuditGraph
@@ -5,38 +7,76 @@ from app.schemas import AuditReport
5
  from app.services.report_formatter import format_report_markdown
6
 
7
 
 
 
 
 
 
 
 
8
  async def analyze_repo(repo_url: str):
9
  if not repo_url.strip():
10
- yield "Paste a public GitHub repository URL to start."
11
  return
12
 
13
- transcript: list[str] = []
 
14
  try:
15
  async for event in AuditGraph().run_with_progress(repo_url.strip()):
16
  if isinstance(event, AuditReport):
17
- transcript.append("")
18
- transcript.append(format_report_markdown(event))
19
  else:
20
- transcript.append(event)
21
- yield "\n".join(transcript)
22
  except Exception as exc:
23
- transcript.append(f"Audit failed: {exc}")
24
- yield "\n".join(transcript)
 
 
 
 
25
 
26
 
27
  def build_app() -> gr.Blocks:
28
  with gr.Blocks(title="SwarmAudit") as demo:
29
- gr.Markdown("# SwarmAudit")
30
- gr.Markdown("Paste any public GitHub URL. Get a structured AI code review in minutes.")
31
- repo_url = gr.Textbox(
32
- label="GitHub Repository URL",
33
- placeholder="https://github.com/owner/repo",
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  )
35
- analyze = gr.Button("Analyze")
36
- output = gr.Markdown(label="Audit Report")
37
- analyze.click(analyze_repo, inputs=repo_url, outputs=output)
 
 
 
 
 
 
 
 
38
  return demo
39
 
40
 
 
 
 
 
 
 
41
  if __name__ == "__main__":
42
- build_app().queue().launch()
 
1
+ import os
2
+
3
  import gradio as gr
4
 
5
  from app.agents.graph import AuditGraph
 
7
  from app.services.report_formatter import format_report_markdown
8
 
9
 
10
+ EXAMPLE_REPOS = {
11
+ "Requests": "https://github.com/psf/requests",
12
+ "ItsDangerous": "https://github.com/pallets/itsdangerous",
13
+ "Flask": "https://github.com/pallets/flask",
14
+ }
15
+
16
+
17
  async def analyze_repo(repo_url: str):
18
  if not repo_url.strip():
19
+ yield "Paste a public GitHub repository URL to start.", ""
20
  return
21
 
22
+ progress: list[str] = []
23
+ report_markdown = ""
24
  try:
25
  async for event in AuditGraph().run_with_progress(repo_url.strip()):
26
  if isinstance(event, AuditReport):
27
+ report_markdown = format_report_markdown(event)
 
28
  else:
29
+ progress.append(event)
30
+ yield "\n".join(progress), report_markdown
31
  except Exception as exc:
32
+ progress.append(f"Audit failed: {exc}")
33
+ yield "\n".join(progress), report_markdown
34
+
35
+
36
+ def choose_example(example_name: str) -> str:
37
+ return EXAMPLE_REPOS.get(example_name, "")
38
 
39
 
40
  def build_app() -> gr.Blocks:
41
  with gr.Blocks(title="SwarmAudit") as demo:
42
+ gr.Markdown(
43
+ "# SwarmAudit\n"
44
+ "Paste a public GitHub URL and get a structured multi-agent audit report."
45
+ )
46
+
47
+ with gr.Row():
48
+ repo_url = gr.Textbox(
49
+ label="GitHub Repository URL",
50
+ placeholder="https://github.com/owner/repo",
51
+ scale=4,
52
+ )
53
+ analyze = gr.Button("Analyze", variant="primary", scale=1)
54
+
55
+ example = gr.Dropdown(
56
+ label="Example repos",
57
+ choices=list(EXAMPLE_REPOS.keys()),
58
+ value=None,
59
+ interactive=True,
60
  )
61
+ example.change(choose_example, inputs=example, outputs=repo_url)
62
+
63
+ with gr.Row():
64
+ progress_output = gr.Textbox(
65
+ label="Agent Progress",
66
+ lines=10,
67
+ interactive=False,
68
+ )
69
+ report_output = gr.Markdown(label="Audit Report")
70
+
71
+ analyze.click(analyze_repo, inputs=repo_url, outputs=[progress_output, report_output])
72
  return demo
73
 
74
 
75
+ def launch_app() -> None:
76
+ server_name = os.getenv("GRADIO_SERVER_NAME", "0.0.0.0")
77
+ server_port = int(os.getenv("PORT", os.getenv("GRADIO_SERVER_PORT", "7860")))
78
+ build_app().queue().launch(server_name=server_name, server_port=server_port)
79
+
80
+
81
  if __name__ == "__main__":
82
+ launch_app()
tests/test_gradio_app.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import runpy
2
+ from pathlib import Path
3
+
4
+ from app.ui.gradio_app import build_app, choose_example, launch_app
5
+
6
+
7
+ def test_choose_example_returns_repo_url():
8
+ assert choose_example("Requests") == "https://github.com/psf/requests"
9
+
10
+
11
+ def test_choose_example_returns_empty_string_for_unknown_choice():
12
+ assert choose_example("Unknown") == ""
13
+
14
+
15
+ def test_build_app_creates_gradio_blocks():
16
+ demo = build_app()
17
+
18
+ assert demo is not None
19
+
20
+
21
+ def test_root_app_py_exposes_demo_for_spaces():
22
+ namespace = runpy.run_path(str(Path(__file__).parents[1] / "app.py"))
23
+
24
+ assert "demo" in namespace
25
+
26
+
27
+ def test_launch_app_uses_spaces_friendly_defaults(monkeypatch):
28
+ calls = {}
29
+
30
+ class FakeQueuedApp:
31
+ def launch(self, **kwargs):
32
+ calls.update(kwargs)
33
+
34
+ class FakeApp:
35
+ def queue(self):
36
+ return FakeQueuedApp()
37
+
38
+ monkeypatch.setattr("app.ui.gradio_app.build_app", lambda: FakeApp())
39
+ monkeypatch.delenv("PORT", raising=False)
40
+ monkeypatch.delenv("GRADIO_SERVER_PORT", raising=False)
41
+ monkeypatch.delenv("GRADIO_SERVER_NAME", raising=False)
42
+
43
+ launch_app()
44
+
45
+ assert calls == {"server_name": "0.0.0.0", "server_port": 7860}