Scope: this document explains how the frontend, Flask UI, FastAPI inference service, Hugging Face Spaces container, GitHub API, and n8n automation work together for the web application phase of the project.
The web application is not a single monolithic page. It is a coordinated stack of:
- a vanilla JavaScript single-page frontend rendered by Flask templates,
- a Flask presentation layer that owns user-facing flows,
- a FastAPI prediction service that loads the DistilBERT multitask model,
- a GitHub API integration layer for label write-back and issue creation,
- and an n8n webhook workflow for event-driven automation.
The design goal is to keep inference, orchestration, and UI concerns separated while still making the end-user experience feel like one product.
💡 NOTE: The architecture is intentionally split across Flask and FastAPI. Flask handles browser interaction and request forwarding, while FastAPI owns the model lifecycle and all ML inference logic. That separation keeps the model service small, testable, and reusable by n8n.
| Component | Runtime / Technology | Responsibility | Key files |
|---|---|---|---|
| Frontend UI | HTML, CSS, vanilla JavaScript | Collects input, renders predictions, drives user interaction | src/templates/index.html |
| Flask app | Flask + requests |
Serves the SPA, validates flow selection, proxies user actions to backend endpoints | src/app.py |
| FastAPI service | FastAPI + Pydantic + HTTPX | Loads the trained model and performs prediction plus GitHub automation operations | src/api/backend.py |
| Model definition | PyTorch + Transformers | Defines the shared DistilBERT backbone and the dual-head classifier | src/models/model_arch.py |
| GitHub API | GitHub REST API | Fetches issues/PRs and writes labels or creates issues | External service |
| n8n workflow | n8n webhook + HTTP Request nodes | Listens for GitHub events, calls the classifier, and writes labels back | automation/PR Classifier n8n workflow.json |
graph TD
U[User] --> UI[Single-page UI\nindex.html]
UI --> F[Flask App\n/src/app.py]
F -->|Flow A / public analysis| API[FastAPI Service\n/src/api/backend.py]
F -->|Flow A / private fetch| API
F -->|Flow B / manual analysis| API
API --> M[MultiTaskDistilBERT\nDistilBERT backbone + 2 heads]
API --> GH[GitHub REST API]
GH --> API
API --> F
F --> UI
GH --> N8N[n8n Webhook Workflow]
N8N --> API
N8N --> GH
This topology shows two important paths:
- interactive analysis through the browser and Flask,
- event-driven automation through GitHub webhooks and n8n.
- The user pastes a public GitHub issue or pull request URL into the UI.
- The frontend validates the URL shape.
- Flask forwards the request to FastAPI.
- FastAPI fetches the issue or PR content from GitHub if needed.
- FastAPI runs the model and returns sentiment, labels, confidence values, and priority metadata.
- If a token is present, the UI exposes an action to apply labels back to GitHub.
- The user types a title and optional body.
- The frontend sends the text to Flask.
- Flask forwards the payload to FastAPI
/predict. - FastAPI performs inference and returns a structured response.
- If a repository and token are provided, the UI can create a new issue with predicted labels.
- GitHub emits a webhook when a new issue or PR is created.
- n8n receives the payload through its webhook trigger.
- n8n normalizes the event into
{title, body, number, repo, owner}. - n8n calls FastAPI
/predict. - n8n keeps only the fields required for write-back.
- n8n posts labels to GitHub through the REST API.
⚠️ WARNING: Flow C is event-driven and assumes the n8n webhook endpoint is publicly reachable. A localhost-only webhook URL will not work for GitHub delivery unless you are tunneling or reverse-proxying traffic.
sequenceDiagram
autonumber
participant U as User
participant UI as Frontend UI
participant F as Flask App
participant API as FastAPI Service
participant GH as GitHub API
U->>UI: Enter URL or title/body
UI->>F: POST /analyze or /fetch-issue or /create-issue
F->>API: Forward analysis request
API->>GH: Fetch repo metadata when needed
GH-->>API: Issue / PR JSON
API-->>F: Prediction payload
F-->>UI: Structured JSON for rendering
UI->>GH: Optional label write-back or issue creation via backend proxy
GH-->>UI: Success / error response
Flask is the browser-facing coordinator. It owns:
- the homepage route,
- input validation for Flow A and Flow B,
- token forwarding from the browser to the backend,
- and result shaping for the SPA.
FastAPI is the model and automation service. It owns:
- the loaded PyTorch checkpoint,
- the tokenizer,
- the inference routine,
- GitHub data fetching,
- label application,
- issue creation,
- and the health endpoint.
This separation keeps the model service independent of presentation concerns and makes the same inference contract usable by both the UI and n8n.
app = Flask(__name__)
app.secret_key = os.environ.get("FLASK_SECRET_KEY", os.urandom(32))
API_BASE = os.environ.get("API_BASE", "http://localhost:8000")
def _forward_to_backend(method, endpoint, token=None, json_data=None):
headers = {}
if token:
headers["X-GitHub-Token"] = token
url = f"{API_BASE}{endpoint}"
resp = requests.request(method=method, url=url, headers=headers, json=json_data, timeout=30)
return jsonify(resp.json()) if resp.ok else (jsonify({"error": resp.text}), resp.status_code)MODEL_PATH = "models/pr_classifier.pt"
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = MultiTaskDistilBERT(num_sentiment=3, num_github=5)
model.load_state_dict(torch.load(MODEL_PATH, map_location=DEVICE))
model.to(DEVICE)
model.eval()
tokenizer = DistilBertTokenizer.from_pretrained("distilbert-base-uncased")These are the two code surfaces that anchor the architecture: Flask moves requests, and FastAPI performs inference.
| Boundary | What crosses it | Risk | Mitigation |
|---|---|---|---|
| Browser -> Flask | URLs, titles, bodies, token value | Input tampering, token exposure in logs | Validate inputs and keep token in session storage only |
| Flask -> FastAPI | JSON payloads and forwarded token header | Misrouted requests or missing auth header | Centralize forwarding logic in one helper |
| FastAPI -> GitHub API | Read and write requests | Rate limits, permission failures, bad scopes | Return explicit HTTP errors and keep token scope minimal |
| GitHub -> n8n | Webhook payloads | Spoofed or malformed events | Use a secure public webhook endpoint and signature validation in hardened deployments |
💡 NOTE: The UI stores the GitHub token in browser session storage, not in the server database. That design avoids persistent server-side token retention, but it still requires careful client-side handling.
The app is best understood as a three-tier runtime:
- Presentation tier: the Flask-served single-page UI.
- Inference tier: the FastAPI model service.
- Automation tier: GitHub webhooks and n8n for background labeling.
The split keeps the application pragmatic: the browser remains lightweight, the model service remains deterministic, and the automation tier remains event-driven.