An AI-powered IT Service Management agent that automates change risk evaluation for GitHub Pull Requests. Built on LangGraph, it listens for GitHub webhook events, runs a deterministic + AI-assisted policy analysis pipeline, and posts risk assessment comments directly on PRs.
Designed around ISO/IEC 20000 change-control principles, the agent enforces configurable policy rules (defined in YAML) to classify every PR as LOW or HIGH risk based on the files it touches.
Demo site: ITSM Agent
- Features
- Architecture
- Tech Stack
- Project Structure
- Prerequisites
- Environment Variables
- Getting Started
- Database Migrations
- Policy Configuration
- API Reference
- Web Dashboard
- GitHub App webhook integration — receives
pull_requestevents and processes them automatically - LangGraph evaluation pipeline — a multi-node state graph that extracts PR data, validates JIRA tickets, runs LLM semantic analysis, applies policy rules, and posts comments
- LLM-powered semantic risk audit — compares JIRA ticket descriptions against code diffs using OpenAI to detect scope creep and unhandled risks
- Deterministic policy engine — YAML-driven risk classification with glob-based file path matching
- JIRA integration — validates ticket numbers in PR titles and fetches metadata via the JIRA API
- Automatic PR commenting — posts risk assessment summaries directly on pull requests via the GitHub API
- Real-time web dashboard — HTMX + SSE-powered UI displaying live evaluation results with pagination (latest per PR)
- Async PostgreSQL persistence — stores evaluation runs and analysis results with full audit trail
- Idempotent evaluations — deduplicates by
owner/repo:pr_number:head_sha:body_hash - In-memory cache with SSE push — a single background task refreshes an in-memory cache; all SSE clients share the result with zero per-client DB load
GitHub Webhook (pull_request)
│
▼
┌───────────────────────┐
│ FastAPI Application │
│ POST /api/v1/github │
│ /webhook │
└──────────┬────────────┘
│ verify HMAC signature
▼
┌───────────────────────────────────────────────────────┐
│ LangGraph State Machine │
│ │
│ read_pr_from_webhook │
│ ▼ │
│ fetch_pr_info (GitHub REST API) │
│ ▼ │
│ analyze_jira_ticket_number (JIRA API validation) │
│ ▼ │
│ ┌────────────────────────┬──────────────────────┐ │
│ │ jira_to_code_llm │ policy_rule_analysis │ │
│ │ (OpenAI semantic audit)│ (YAML policy engine) │ │
│ └────────────┬───────────┴──────────┬───────────┘ │
│ ▼ ▼ │
│ post_pr_comment (GitHub REST API) │
└──────────────────┬────────────────────────────────────┘
│
▼
┌──────────────────────┐ ┌─────────────────────┐
│ PostgreSQL (async) │◄────►│ Web Dashboard │
│ evaluation_run │ │ HTMX + SSE │
│ analysis_result │ │ /evaluations │
└──────────────────────┘ └─────────────────────┘
The application owns a single httpx.AsyncClient for the full process lifetime.
FastAPI lifespan
-> app.state.http_client
-> get_http_client(request)
-> get_github_client(client)
-> get_jira_client(client)
-> get_evaluation_service(session, client)
-> LangGraph state.http_client
-> GitHub and JIRA nodes
This keeps outbound HTTP usage explicit and testable. Route handlers use FastAPI dependencies, while LangGraph nodes receive the shared client through AgentState when the evaluation workflow is invoked.
| Layer | Technology |
|---|---|
| Web Framework | FastAPI + Uvicorn |
| AI Orchestration | LangGraph / LangChain / OpenAI |
| Database | PostgreSQL (asyncpg + SQLAlchemy + SQLModel) |
| Migrations | Alembic |
| GitHub Auth | GitHub App (JWT + installation tokens via PyJWT) |
| Frontend | Jinja2 templates + HTMX + SSE (sse-starlette) |
| Reverse Proxy | Caddy (automatic HTTPS) |
| Package Manager | uv |
| Containerisation | Docker + Docker Compose |
├── .dockerignore
├── .env.example
├── .gitignore
├── Caddyfile
├── Dockerfile
├── README.md
├── alembic.ini
├── alembic/
│ ├── README
│ ├── env.py
│ ├── script.py.mako
│ └── versions/ # Database migration scripts
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI app entry point
│ ├── api/
│ │ ├── __init__.py
│ │ ├── api_v1.py # API v1 router
│ │ └── endpoints/
│ │ ├── __init__.py
│ │ ├── github.py # GitHub webhook endpoint (signature verification, event routing)
│ │ └── health.py # Health check endpoint
│ ├── core/
│ │ ├── __init__.py
│ │ ├── config.py # Pydantic Settings (env vars)
│ │ ├── lifespan.py # App startup/shutdown lifecycle and shared httpx client
│ │ ├── llm.py # LLM client initialization
│ │ ├── logging.py # Centralized logging
│ │ └── security.py # HMAC SHA-256 signature verification
│ ├── db/
│ │ ├── __init__.py
│ │ ├── session.py # Async engine & session factory
│ │ └── models/
│ │ ├── __init__.py
│ │ ├── evaluation_run.py # EvaluationRun ORM model
│ │ └── analysis_result.py # AnalysisResult ORM model
│ ├── dependencies/
│ │ ├── __init__.py
│ │ ├── database.py # FastAPI database dependency
│ │ ├── http.py # Shared AsyncClient dependency from app.state
│ │ └── services.py # Service/integration constructors for DI
│ ├── integrations/
│ │ ├── __init__.py
│ │ ├── github/
│ │ │ ├── __init__.py
│ │ │ ├── auth.py # GitHub App JWT exchange using injected httpx client
│ │ │ └── client.py # GitHub REST API client with explicit AsyncClient dependency
│ │ └── jira/
│ │ ├── __init__.py
│ │ └── client.py # JIRA REST API client with explicit AsyncClient dependency
│ ├── services/
│ │ ├── __init__.py
│ │ └── change_management/
│ │ ├── __init__.py
│ │ ├── cache.py # In-memory evaluation cache
│ │ ├── cache_updater.py # Background cache refresh task
│ │ ├── evaluations.py # Evaluation orchestration service
│ │ ├── graph.py # LangGraph workflow definition
│ │ ├── notifier.py # In-process asyncio notification
│ │ ├── prompts.py # LLM prompt templates
│ │ ├── state.py # AgentState model, including shared HTTP client field for graph nodes
│ │ ├── nodes/
│ │ │ ├── __init__.py
│ │ │ ├── pr_io.py # Webhook parsing, PR fetch, comment posting
│ │ │ ├── analysis.py # JIRA ticket and policy rule analysis
│ │ │ ├── llm_analysis.py # LLM semantic risk audit (JIRA vs code diff)
│ │ │ └── utils.py # Shared node utilities (make_result)
│ │ └── policy/
│ │ ├── __init__.py
│ │ ├── policy.yaml # Risk classification rules
│ │ ├── loader.py # YAML policy loader
│ │ ├── types.py # ChangeTypeRule data model
│ │ └── priority.py # Risk priority helpers
│ ├── web/
│ │ ├── __init__.py
│ │ └── router.py # HTMX pages and SSE stream endpoints
│ ├── templates/
│ │ ├── base.html # Base layout template
│ │ ├── evaluations.html # Evaluations page template
│ │ ├── index.html # Home page template
│ │ └── partials/
│ │ ├── dashboard.html # Dashboard partial
│ │ ├── evaluations_latest.html # Latest evaluations partial
│ │ └── evaluations_list.html # Evaluations list partial
│ └── static/
│ ├── css/
│ │ └── vendor/ # Vendored CSS (DaisyUI)
│ └── js/
│ └── vendor/ # Vendored JS (HTMX, Alpine.js, Tailwind CSS)
├── docs/
│ └── SETUP_GUIDE.md
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Pytest fixtures & shared test setup
│ ├── test_cache.py # Cache tests
│ ├── test_evaluations.py # Evaluation service tests
│ ├── test_github_auth.py # GitHub auth tests
│ ├── test_github_client.py # GitHub client tests
│ ├── test_jira_client.py # JIRA client tests
│ ├── test_nodes.py # LangGraph node tests
│ ├── test_security.py # HMAC security tests
│ └── test_webhook_service.py # Webhook service tests
├── docker-compose.yaml
├── pyproject.toml
└── uv.lock
The main DI boundary lives in app/dependencies/: request handlers resolve shared infrastructure from FastAPI, while the evaluation service passes the same HTTP client into the LangGraph state for non-route nodes.
- Python 3.13+
- uv — fast Python package manager
- PostgreSQL — local instance or managed service
- GitHub App — with webhook configured for
pull_requestevents - OpenAI API key
Create a .env file in the project root:
# Database
DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/itsm_agent
# OpenAI
OPENAI_API_KEY=sk-...
# JIRA
JIRA_BASE_URL=https://your-domain.atlassian.net
JIRA_EMAIL=your-email@example.com
JIRA_API_TOKEN=your-jira-api-token
# GitHub App
GITHUB_APP_ID=123456
GITHUB_APP_PRIVATE_KEY=/path/to/private-key.pem # or inline PEM string
GITHUB_WEBHOOK_SECRET=your-webhook-secret| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | Async PostgreSQL connection string (postgresql+asyncpg://...) |
OPENAI_API_KEY |
Yes | OpenAI API key for LLM-based semantic risk analysis |
JIRA_BASE_URL |
Yes | JIRA instance URL (e.g. https://your-domain.atlassian.net) |
JIRA_EMAIL |
Yes | Email associated with the JIRA API token |
JIRA_API_TOKEN |
Yes | JIRA API token for ticket validation |
GITHUB_APP_ID |
Yes | Your GitHub App's ID |
GITHUB_APP_PRIVATE_KEY |
Yes | Path to PEM file or inline key (escaped \n supported) |
GITHUB_WEBHOOK_SECRET |
Yes | Secret used to verify webhook HMAC signatures |
Note:
GITHUB_APP_PRIVATE_KEYaccepts either a file path or an inline PEM string. When using a file path, the app reads the file at startup.
-
Clone the repository
git clone https://github.com/your-org/itsm-agent.git cd itsm-agent -
Install dependencies with uv
uv sync
-
Set up the environment
cp .env.example .env # or create .env manually (see Environment Variables above) -
Start PostgreSQL
Use an existing instance, or uncomment the
dbservice indocker-compose.yamlfor a local container:docker compose up db -d
-
Run database migrations
uv run alembic upgrade head
-
Start the development server
uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
The app is now available at http://localhost:8000.
The production setup uses Docker Compose with Caddy as the reverse proxy (automatic HTTPS).
-
Prepare the environment
# Place your GitHub App private key in the project root cp /path/to/private-key.pem ./pr-comment-bot-key.2026-01-28.pem # Create .env with all required variables
-
Build and start
docker compose up -d --build
This starts:
- backend — the FastAPI application on port 8000 (internal)
- caddy — reverse proxy on ports 80/443 with automatic TLS
-
Run migrations (first time or after model changes)
docker compose exec backend alembic upgrade head -
Check health
curl https://your-domain.com/api/v1/health # {"status": "ok"}
Migrations are managed by Alembic with async PostgreSQL support.
# Apply all pending migrations
uv run alembic upgrade head
# Create a new migration after model changes
uv run alembic revision --autogenerate -m "describe your change"
# Downgrade one revision
uv run alembic downgrade -1
# View migration history
uv run alembic historyRisk classification rules are defined in app/services/change_management/policy/policy.yaml. The policy engine uses glob-based file path matching to determine risk levels.
Example:
risk_levels:
LOW:
description: "Low risk change"
change_types: {}
HIGH:
description: "High risk change"
change_types:
db:
description: "Database changes"
path_patterns:
- "alembic/migrations/**"
- "app/db/**"
infra:
description: "Infrastructure changes"
path_patterns:
- "Dockerfile"
- "docker-compose.yaml"When a PR's changed files match a HIGH-risk path pattern, the agent flags the PR accordingly and posts a risk summary as a PR comment.
The agent also checks for a JIRA ticket number (pattern ABC-1234) in the PR title. Missing tickets are flagged as HIGH risk.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/health |
Health check |
| POST | /api/v1/github/webhook |
GitHub webhook receiver |
| GET | / |
Web dashboard (HTML) |
| GET | /evaluations |
Paginated evaluations list (HTML) |
| GET | /evaluations/sse-stream |
SSE stream for real-time eval updates |
The GitHub webhook endpoint expects:
| Header | Description |
|---|---|
X-GitHub-Event |
Event type (e.g. pull_request) |
X-Hub-Signature-256 |
HMAC SHA-256 signature |
The app includes an HTMX-powered web dashboard at the root URL (/):
- Home — shows the latest evaluation per PR with real-time SSE updates
- Evaluations (
/evaluations) — paginated list of latest evaluations per PR with risk levels, statuses, and analysis details
The dashboard updates in real-time via Server-Sent Events without polling the database — a background cache updater task refreshes an in-memory cache, and all connected SSE clients receive updates simultaneously.