A Rails + Python application for analyzing Path of Exile 2 league changes and helping with build planning.
The long-term goal is to build an assistant that can:
- ingest new league announcements and patch notes
- detect what got buffed, nerfed, reworked, added, or removed
- map those changes to known build archetypes
- help diagnose a struggling character
- recommend better passive pathing, uniques, and rare item priorities
This project is not intended to automate gameplay or act as a bot. It is a planning and analysis tool.
- Overview
- Goals of the App
- Tech Stack
- Project Layout
- Current MVP Scope
- Quick Start
- Detailed Setup
- Database and Core Models
- Example API Endpoints
- Development Workflow
- Debugging / Issues We Ran Into
- Recommended Next Steps
- Long-Term Vision
- Notes
POE2 Build AI is meant to become a build analysis assistant for Path of Exile 2.
Instead of trying to train a giant game-specific model from scratch, the app is structured around:
- official patch-note ingestion
- structured build archetype data
- character snapshot diagnosis
- rule-based and model-assisted recommendations
The first versions focus on building a reliable foundation:
- store patch documents
- store balance changes
- store build archetypes
- diagnose characters
- expose simple recommendation APIs
This app is meant to answer questions like:
- What builds got buffed or nerfed this league?
- Which league starters look stronger after the latest patch notes?
- Why does my build feel weak right now?
- What should I upgrade next?
- Should I replace this unique with a rare?
- What stats should I prioritize on my next gear upgrade?
The app is built around a few core ideas:
- store official patch notes and announcements
- extract meaningful balance changes from raw text
- maintain a structured database of build archetypes
- track offense/defense tags, scaling methods, and common failure modes
- accept a snapshot of a build
- identify likely issues
- suggest next upgrades and fixes
- combine structured data, rules, and later ML/LLM support
- explain why a build is improving or getting worse
- Ruby on Rails 8
- PostgreSQL
- Redis
- Sidekiq
- Python / FastAPI
- scikit-learn / XGBoost for future scoring models
- Docker Compose
poe2_build_ai/
├── app/ # Rails application
│ ├── app/
│ │ ├── controllers/
│ │ ├── models/
│ │ ├── services/
│ │ └── jobs/
│ ├── config/
│ ├── db/
│ └── Dockerfile
├── python_service/ # FastAPI scoring service
│ ├── main.py
│ ├── requirements.txt
│ └── Dockerfile
├── docker-compose.yml
├── .env
└── README.md
Responsible for:
- API endpoints
- database models
- background jobs
- patch ingestion
- archetype storage
- character snapshots
- recommendation history
Responsible for:
- lightweight scoring endpoints
- future ranking or model-based recommendations
Stores:
- patch documents
- patch changes
- build archetypes
- archetype impact mappings
- character snapshots
- recommendation runs
Used for:
- background jobs
- patch fetching and parsing
- future async processing
The current app skeleton is designed to support:
- storing patch documents
- storing patch changes
- storing build archetypes
- diagnosing a build snapshot
- exposing basic JSON API endpoints
Planned next steps include:
- patch note ingestion jobs
- patch parsing
- change classification
- mapping changes to archetypes
- league starter summaries
- upgrade recommendation logic
- passive-tree-aware recommendations
If you already have Docker Desktop running and just want the short version:
mkdir poe2_build_ai
cd poe2_build_ai
git init
mkdir -p python_serviceCreate the Rails app:
docker run --rm \
-v "$PWD:/app" \
-w /app \
ruby:3.3.1 \
bash -lc 'gem install rails bundler && ruby -S rails new app -d postgresql'Then create the project files described below, and run:
docker compose build
docker compose run --rm web bundle install
docker compose run --rm web bin/rails db:create
docker compose run --rm web bin/rails db:migrate
docker compose upHealth checks:
curl http://localhost:3000/up
curl http://localhost:8001/healthYou should have these installed on your Mac:
- Docker Desktop
- Git
Optional but useful:
- Homebrew
- Python 3
- curl
Verify Docker is running:
docker --version
docker compose versionFrom your terminal:
mkdir poe2_build_ai
cd poe2_build_ai
git initWe initially scaffolded Rails inside Docker rather than installing Ruby/Rails directly on macOS.
docker run --rm \
-v "$PWD:/app" \
-w /app \
ruby:3.3.1 \
bash -lc 'gem install rails bundler && ruby -S rails new app -d postgresql'If ruby -S rails is needed, it is because the rails executable may not be on PATH inside the temporary container.
mkdir -p python_serviceCreate python_service/requirements.txt:
fastapi
uvicorn
pydantic
scikit-learn
pandas
numpy
xgboostCreate python_service/main.py:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class BuildScoreRequest(BaseModel):
archetype: dict
patch_impacts: list
character_snapshot: dict | None = None
@app.get("/health")
def health():
return {"ok": True}
@app.post("/score_archetype")
def score_archetype(req: BuildScoreRequest):
score = sum(item.get("impact_score", 0) for item in req.patch_impacts)
return {
"league_start_score": score,
"confidence": 0.65
}Create python_service/Dockerfile:
FROM python:3.11-slim
WORKDIR /service
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8001
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]Replace app/Dockerfile with:
FROM ruby:3.3.1
RUN apt-get update -qq && apt-get install -y \
build-essential \
libpq-dev \
nodejs \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /rails
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . .
EXPOSE 3000
CMD ["bash", "-lc", "bundle exec rails server -b 0.0.0.0 -p 3000"]Replace app/Gemfile with:
source "https://rubygems.org"
gem "rails", "~> 8.1.3"
gem "pg", "~> 1.1"
gem "puma", ">= 5.0"
gem "bootsnap", require: false
gem "propshaft"
gem "sidekiq"
gem "redis"
gem "httparty"
gem "oj"
gem "dotenv-rails"
gem "pgvector"
gem "ruby-openai"propshaft is included because Rails 8 generated config may still assume asset-related configuration exists.
Create this in the project root:
services:
db:
image: pgvector/pgvector:pg16
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: poe2_ai_development
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7
ports:
- "6379:6379"
python_service:
build:
context: ./python_service
volumes:
- ./python_service:/service
ports:
- "8001:8001"
web:
build:
context: ./app
command: bash -lc "rm -f tmp/pids/server.pid && bundle exec rails server -b 0.0.0.0 -p 3000"
volumes:
- ./app:/rails
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://postgres:password@db:5432/poe2_ai_development
REDIS_URL: redis://redis:6379/0
PYTHON_SERVICE_URL: http://python_service:8001
RAILS_ENV: development
depends_on:
- db
- redis
- python_service
worker:
build:
context: ./app
command: bash -lc "bundle exec sidekiq"
volumes:
- ./app:/rails
environment:
DATABASE_URL: postgres://postgres:password@db:5432/poe2_ai_development
REDIS_URL: redis://redis:6379/0
PYTHON_SERVICE_URL: http://python_service:8001
RAILS_ENV: development
depends_on:
- db
- redis
volumes:
postgres_data:Replace app/config/database.yml with:
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
url: <%= ENV["DATABASE_URL"] %>
development:
<<: *default
test:
<<: *default
url: postgres://postgres:password@db:5432/poe2_ai_testCreate app/config/initializers/sidekiq.rb:
Sidekiq.configure_server do |config|
config.redis = { url: ENV.fetch("REDIS_URL", "redis://redis:6379/0") }
end
Sidekiq.configure_client do |config|
config.redis = { url: ENV.fetch("REDIS_URL", "redis://redis:6379/0") }
endCreate .env:
OPENAI_API_KEY=replace_meBuild the containers:
docker compose buildInstall gems inside the web container:
docker compose run --rm web bundle installCreate the database:
docker compose run --rm web bin/rails db:createRun migrations:
docker compose run --rm web bin/rails db:migrateStart the app:
docker compose upPlanned core models include:
PatchDocumentPatchChangeArchetypeArchetypeImpactCharacterSnapshotRecommendationRun
These are intended to support:
- patch note storage
- parsed balance changes
- build archetypes
- build impact analysis
- build diagnosis
- recommendation tracking
docker compose run --rm web bin/rails generate model PatchDocument \
title:string source_url:string version:string document_type:string \
published_at:datetime raw_text:text metadata:jsonb
docker compose run --rm web bin/rails generate model PatchChange \
patch_document:references entity_name:string entity_type:string \
change_type:string before_text:text after_text:text summary:text \
tags:jsonb numeric_data:jsonb confidence:float
docker compose run --rm web bin/rails generate model Archetype \
name:string class_name:string ascendancy_name:string primary_skill:string \
offense_tags:jsonb defense_tags:jsonb core_mechanics:jsonb \
leveling_notes:text failure_modes:text
docker compose run --rm web bin/rails generate model ArchetypeImpact \
archetype:references patch_change:references impact_score:float \
impact_kind:string reasoning:text
docker compose run --rm web bin/rails generate model CharacterSnapshot \
name:string class_name:string ascendancy_name:string level:integer \
skills:jsonb stats:jsonb defenses:jsonb gear:jsonb passives:jsonb \
constraints:jsonb
docker compose run --rm web bin/rails generate model RecommendationRun \
character_snapshot:references mode:string input_payload:jsonb output_payload:jsonbGenerate the migration:
docker compose run --rm web bin/rails generate migration EnablePgvectorThen edit the migration file to:
class EnablePgvector < ActiveRecord::Migration[8.1]
def change
enable_extension "vector"
end
endWhen editing generated migrations, use defaults like:
t.jsonb :metadata, default: {}
t.jsonb :tags, default: []
t.jsonb :numeric_data, default: {}
t.jsonb :offense_tags, default: []
t.jsonb :defense_tags, default: []
t.jsonb :core_mechanics, default: []
t.jsonb :skills, default: {}
t.jsonb :stats, default: {}
t.jsonb :defenses, default: {}
t.jsonb :gear, default: {}
t.jsonb :passives, default: {}
t.jsonb :constraints, default: {}
t.jsonb :input_payload, default: {}
t.jsonb :output_payload, default: {}GET /upGET /api/league_startersPOST /api/diagnose_buildExample request:
{
"name": "My Ranger",
"class_name": "Ranger",
"ascendancy_name": "Deadeye",
"level": 35,
"defenses": {
"life": 420,
"fire_res": 48,
"cold_res": 31,
"lightning_res": 75
},
"gear": {
"weapon": {
"type": "Bow",
"dps_score": 70
}
}
}Example curl:
curl -X POST http://localhost:3000/api/diagnose_build \
-H "Content-Type: application/json" \
-d '{
"name": "My Ranger",
"class_name": "Ranger",
"ascendancy_name": "Deadeye",
"level": 35,
"defenses": {
"life": 420,
"fire_res": 48,
"cold_res": 31,
"lightning_res": 75
},
"gear": {
"weapon": {
"type": "Bow",
"dps_score": 70
}
}
}'Common commands:
docker compose up
docker compose down
docker compose logs -f web
docker compose logs -f worker
docker compose run --rm web bin/rails console
docker compose run --rm web bin/rails db:migrate
docker compose run --rm web bin/rails db:seed
docker compose restart webUseful checks:
curl http://localhost:3000/up
curl http://localhost:8001/health
curl http://localhost:3000/api/league_startersThis section documents the actual problems hit during setup so future setup is faster.
Command:
docker run --rm \
-v "$PWD:/app" \
-w /app \
ruby:3.3.1 \
bash -lc 'gem install rails bundler && rails new app -d postgresql'Problem:
- Rails gem installed successfully
- but the
railsexecutable was not on PATH inside the shell
Fix:
- use
ruby -S railsinstead
docker run --rm \
-v "$PWD:/app" \
-w /app \
ruby:3.3.1 \
bash -lc 'gem install rails bundler && ruby -S rails new app -d postgresql'Problem:
- the Rails app scaffold was mostly created
- but some post-generation installer steps failed while loading
bootsnap/setup
Cause:
- dependency/setup mismatch during scaffold completion
Fix:
- keep the generated app
- continue with Docker-first setup
- ensure
bootsnapis present in the Gemfile - run
bundle installinside the container
Error looked like:
NoMethodError: undefined method `assets' for an instance of Rails::Application::ConfigurationCause:
- Rails generated config still referenced asset configuration
- but the simplified Gemfile no longer had the matching asset pipeline support
Fix:
- add
propshaftback to the Gemfile
gem "propshaft"Problematic controller:
class ApplicationController < ActionController::Base
allow_browser versions: :modern
stale_when_importmap_changes
endCause:
- default scaffold code expected importmap-related helpers
- but the app was no longer using that setup
Fix:
- simplify the controller
class ApplicationController < ActionController::Base
allow_browser versions: :modern
endIf allow_browser ever becomes a problem too, the fallback is:
class ApplicationController < ActionController::Base
endOnce the app boots cleanly:
- add the core models and migrations
- seed a small set of build archetypes
- add a
CharacterDoctorservice - add recommendation endpoints
- add patch-note ingestion jobs
- parse raw patch notes into structured changes
- map those changes to build archetypes
- generate league-start summaries
A good first milestone is getting these endpoints working:
GET /upGET /api/league_startersPOST /api/diagnose_build
The final version of this app should be able to:
- ingest official POE2 patch notes quickly
- classify changes automatically
- explain build meta shifts
- rank likely league starters
- help diagnose weak builds
- recommend item and passive path changes
- provide grounded reasoning instead of generic chatbot answers
The system should stay focused on being a build planning and analysis assistant, not an automation tool.
- This project currently favors a Docker-first workflow
- The app is still in early scaffolding / MVP stage
- The current priority is getting a clean vertical slice working before adding more advanced AI or ML features
- If setup gets weird, favor keeping the generated app and fixing config incrementally instead of repeatedly recreating the project from scratch