When a client asks us for "something with AI" inside an existing Laravel or WordPress site, the first design question is rarely about the model. It is about where the Python goes.
Most of the useful machine-learning and document-processing libraries live in Python. Most of our client sites do not. The sensible answer is usually a small Python service next to the site, not a rewrite. This post is how we would set that up, and where we would stop.
When a Python service is worth it
We add one when the job needs something PHP does badly or not at all:
- Reading messy input: scanned invoices, PDFs with tables, supplier spreadsheets with inconsistent columns.
- Calling a model with retries, timeouts and structured output, where we want proper async code and a real validation layer.
- Heavy batch work such as re-indexing thousands of documents, which should not share a worker pool with checkout requests.
- Data cleaning and reporting that a client's analyst could later read and change.
We do not add one for a single API call to a model provider. A Laravel job or a WordPress plugin can make that call. Another deployable thing means another thing to monitor, patch and pay for, so it has to earn its place.
Why FastAPI for the service
For this shape of work we reach for FastAPI. Request and response bodies are Pydantic models, so the contract between the site and the service is written down in code and checked on every call. It generates an OpenAPI description we can hand to the client's other developers. Async endpoints suit the work, since most of the time is spent waiting on a model or a storage bucket.
The service stays small: a few endpoints, no user accounts, no database of its own unless the job needs one. The main site remains the system of record.
The boundary between site and service
This is where projects go wrong, so we are strict about it.
- The site owns users, permissions and data. The Python service never decides who may see what. The site decides first, then asks the service to do work.
- Calls are server to server. The browser never talks to the Python service. A shared secret or signed token travels between the two, and the service is not exposed to the public internet if we can avoid it.
- Slow work is a job, not a request. The site posts the job, gets an id back, and a queue worker polls or receives a callback. Nobody's page should hang for forty seconds because a PDF was large.
- Every response is validated. If the service returns something that does not match the schema, the site treats it as a failure and shows a plain message. It does not render half an answer.
In Laravel this is a queued job plus a small HTTP client class with a timeout and a retry policy. In WordPress it is a plugin that schedules work through the action queue and stores the result as post meta or a custom table. In both cases the page the user sees is rendered by the site, from stored results.
An example: supplier invoices into a Laravel back office
Say a distributor receives supplier invoices as PDFs by email and a person retypes them into the back office. A version we would build:
- Laravel stores the upload and creates an "extraction" record with status
queued. - A queue worker sends the file to the FastAPI service.
- The service extracts text and tables, asks a model to map fields into a fixed schema (supplier, invoice number, date, line items, totals), and returns JSON.
- Pydantic rejects anything malformed. We also check arithmetic ourselves: line items must add up to the stated total, otherwise the record is flagged.
- Laravel shows the extracted fields next to the original PDF. A person confirms or corrects, then saves.
The model does the tedious reading. A person still approves the result before it touches accounting data. That review step is the design, not a temporary stage we plan to remove.
Cost and reliability, in practice
Model calls cost money and fail sometimes. A few habits keep that manageable:
- Cap spend per job. Set a maximum file size and page count, and refuse early with a clear message.
- Cache by file hash. If the same invoice is uploaded twice, do not pay twice.
- Log inputs and outputs for each job (not secrets, and with the client's retention rules applied) so that when someone says "it read the wrong total" we can see what happened.
- Keep a manual path. If the service is down, the user can still type the invoice in as they do today.
- Make the model swappable. The service wraps the provider behind one function. Changing provider or model should be a configuration change plus a test run, not a rewrite.
Deployment, kept boring
For most clients one small container on the same host or cloud account as the site is enough. We pin dependencies, run the service behind the same firewall rules as the database, add a health endpoint, and send errors to the same place the site's errors go. If the client has no one to look after containers, we say so early, because it changes the cost of the whole project.
When we would say no
If the real problem is that staff are retyping data because the old system has no import, fix the import first. If the volumes are small enough that a person can do it in an hour a week, automation may not pay for itself. We would rather tell a client that after a short call than sell a service they will not use.
What to ask us for
If you run a Laravel or WordPress site and have a manual, document-heavy step that eats staff time, send us three or four real samples and a description of who checks the result today. From that we can say whether a Python service makes sense, what it would roughly involve, and what the human review step looks like. You can reach us through the contact page.