Skip to content

Latest commit

 

History

History
203 lines (146 loc) · 8.04 KB

File metadata and controls

203 lines (146 loc) · 8.04 KB

🌐 System Architecture - Dual-Head PR/Issue Classifier Web App

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.


🎯 Architecture Goal

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.


🧱 System Components

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

🗺️ High-Level Topology

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
Loading

This topology shows two important paths:

  • interactive analysis through the browser and Flask,
  • event-driven automation through GitHub webhooks and n8n.

🔁 Main Execution Flows

Flow A: GitHub URL analysis

  1. The user pastes a public GitHub issue or pull request URL into the UI.
  2. The frontend validates the URL shape.
  3. Flask forwards the request to FastAPI.
  4. FastAPI fetches the issue or PR content from GitHub if needed.
  5. FastAPI runs the model and returns sentiment, labels, confidence values, and priority metadata.
  6. If a token is present, the UI exposes an action to apply labels back to GitHub.

Flow B: Manual title + body analysis

  1. The user types a title and optional body.
  2. The frontend sends the text to Flask.
  3. Flask forwards the payload to FastAPI /predict.
  4. FastAPI performs inference and returns a structured response.
  5. If a repository and token are provided, the UI can create a new issue with predicted labels.

Flow C: n8n automation

  1. GitHub emits a webhook when a new issue or PR is created.
  2. n8n receives the payload through its webhook trigger.
  3. n8n normalizes the event into {title, body, number, repo, owner}.
  4. n8n calls FastAPI /predict.
  5. n8n keeps only the fields required for write-back.
  6. 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.


📡 Request / Response Sequence

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
Loading

🖥️ Why Two Backend Layers Exist

Flask responsibilities

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 responsibilities

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.


🧩 Code Anchors

Flask app bootstrap and API proxying

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)

FastAPI model loading and inference service setup

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.


🔐 Trust Boundaries

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.


✅ Architecture Summary

The app is best understood as a three-tier runtime:

  1. Presentation tier: the Flask-served single-page UI.
  2. Inference tier: the FastAPI model service.
  3. 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.