--- a +++ b/docs/advanced-tutorials/fastapi.md @@ -0,0 +1,216 @@ +# Deploying as an API + +In this section, we will see how you can deploy your pipeline as a REST API using the power of [FastAPI](https://fastapi.tiangolo.com/). + +## The NLP pipeline + +Let's create a simple NLP model, that can: + +- match synonyms of COVID19 +- check for negation, speculation and reported speech. + +You know the drill: + +```python title="pipeline.py" +import edsnlp, edsnlp.pipes as eds + +nlp = edsnlp.blank('eds') +nlp.add_pipe(eds.sentences()) +nlp.add_pipe( + eds.matcher( + regex=dict( + covid=[ + "covid", + r"covid[-\s]?19", + r"sars[-\s]?cov[-\s]?2", + r"corona[-\s]?virus", + ], + ), + attr="LOWER", + ), +) +nlp.add_pipe(eds.negation()) +nlp.add_pipe(eds.family()) +nlp.add_pipe(eds.hypothesis()) +nlp.add_pipe(eds.rspeech()) +``` + +## Creating the FastAPI app + +FastAPI is a incredibly efficient framework, based on Python type hints from the ground up, +with the help of [Pydantic](https://pydantic-docs.helpmanual.io/) (another great library for building modern Python). +We won't go into too much detail about FastAPI in this tutorial. +For further information on how the framework operates, go to its [excellent documentation](https://fastapi.tiangolo.com/)! + +We'll need to create two things: + +1. A module containing the models for inputs and outputs. +2. The script that defines the application itself. + +```python title="models.py" +from typing import List + +from pydantic import BaseModel + + +class Entity(BaseModel): # (1) + + # OMOP-style attributes + start: int + end: int + label: str + lexical_variant: str + normalized_variant: str + + # Qualifiers + negated: bool + hypothesis: bool + family: bool + reported_speech: bool + + +class Document(BaseModel): # (2) + text: str + ents: List[Entity] +``` + +1. The `Entity` model contains attributes that define a matched entity, as well as variables that contain the output of the qualifier components. +2. The `Document` model contains the input text, and a list of detected entities + +Having defined the output models and the pipeline, we can move on to creating the application itself: + +```{ .python .no-check title="app.py" } +from typing import List + +from fastapi import FastAPI + +from pipeline import nlp +from models import Entity, Document + + +app = FastAPI(title="EDS-NLP", version=edsnlp.__version__) + + +@app.post("/covid", response_model=List[Document]) # (1) +async def process( + notes: List[str], # (2) +): + + documents = [] + + for doc in nlp.pipe(notes): + entities = [] + + for ent in doc.ents: + entity = Entity( + start=ent.start_char, + end=ent.end_char, + label=ent.label_, + lexical_variant=ent.text, + normalized_variant=ent._.normalized_variant, + negated=ent._.negation, + hypothesis=ent._.hypothesis, + family=ent._.family, + reported_speech=ent._.reported_speech, + ) + entities.append(entity) + + documents.append( + Document( + text=doc.text, + ents=entities, + ) + ) + + return documents +``` + +1. By telling FastAPI what output format is expected, you get automatic data validation. +2. In FastAPI, input and output schemas are defined through Python type hinting. + Here, we tell FastAPI to expect a list of strings in the `POST` request body. + As a bonus, you get data validation for free. + +## Running the API + +Our simple API is ready to launch! We'll just need to install FastAPI along with a ASGI server to run it. This can be done in one go: + +<div class="termy"> + +```console +$ pip install 'fastapi[uvicorn]' +---> 100% +color:green Successfully installed fastapi +``` + +</div> + +Launching the API is trivial: + +<div class="termy"> + +```console +$ uvicorn app:app --reload +``` + +</div> + +Go to [`localhost:8000/docs`](http://localhost:8000/docs) to admire the automatically generated documentation! + +## Using the API + +You can try the API directly from the documentation. Otherwise, you may use the `requests` package: + +```{ .python .no-check } +import requests + +notes = [ + "Le père du patient n'est pas atteint de la covid.", + "Probable coronavirus.", +] + +r = requests.post( + "http://localhost:8000/covid", + json=notes, +) + +r.json() +``` + +You should get something like: + +```json +[ + { + "text": "Le père du patient n'est pas atteint de la covid.", + "ents": [ + { + "start": 43, + "end": 48, + "label": "covid", + "lexical_variant": "covid", + "normalized_variant": "covid", + "negated": true, + "hypothesis": false, + "family": true, + "reported_speech": false + } + ] + }, + { + "text": "Probable coronavirus.", + "ents": [ + { + "start": 9, + "end": 20, + "label": "covid", + "lexical_variant": "coronavirus", + "normalized_variant": "coronavirus", + "negated": false, + "hypothesis": true, + "family": false, + "reported_speech": false + } + ] + } +] +```