.. crate_anon/docs/source/nlp/nlprp.rst
.. Copyright (C) 2015, University of Cambridge, Department of Psychiatry.
Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
.
This file is part of CRATE.
.
CRATE is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
.
CRATE is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
You should have received a copy of the GNU General Public License
along with CRATE. If not, see .
.. _ANSI SQL: http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt
.. _authentication: https://en.wikipedia.org/wiki/Authentication
.. _authorization: https://en.wikipedia.org/wiki/Authorization
.. _GATE: https://gate.ac.uk/
.. _Grails: https://grails.org/
.. _HTTP: https://tools.ietf.org/html/rfc2616.html
.. _HTTP Accept-Encoding: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
.. _HTTP basic access authentication: https://en.wikipedia.org/wiki/Basic_access_authentication
.. _HTTP Content-Encoding: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
.. _HTTP digest access authentication: https://en.wikipedia.org/wiki/Digest_access_authentication
.. _ISO-8601: https://en.wikipedia.org/wiki/ISO_8601
.. _INFORMATION_SCHEMA: https://en.wikipedia.org/wiki/Information_schema
.. _JSON: https://www.json.org/
.. _Microsoft SQL Server: https://en.wikipedia.org/wiki/Microsoft_SQL_Server
.. _MySQL: https://www.mysql.com/
.. _OAuth: https://en.wikipedia.org/wiki/OAuth
.. _Oracle: https://en.wikipedia.org/wiki/Oracle_Database
.. _PostgreSQL: https://www.postgresql.org/
.. _RESTful: https://en.wikipedia.org/wiki/Representational_state_transfer
.. _Semantic Versioning: http://www.semver.org/
.. _SQLAlchemy: https://www.sqlalchemy.org/
.. _SQLite: https://www.sqlite.org/
.. _URL query string: https://en.wikipedia.org/wiki/Query_string
.. _UTC: https://en.wikipedia.org/wiki/Coordinated_Universal_Time
.. _nlprp:
Natural Language Processing Request Protocol (NLPRP)
----------------------------------------------------
**Version 0.3.0**
.. contents::
:local:
Authors
~~~~~~~
In alphabetical order:
- Rudolf N. Cardinal (RNC), University of Cambridge, 2017-.
- Joe Kearney (JK), University of Cambridge, 2018-19.
- Angus Roberts (AR), King's College London, 2018-.
- Ian Roberts (IR), University of Sheffield, 2018-.
- Francesca Spivack (FS), University of Cambridge, 2018-20.
Rationale
~~~~~~~~~
In the context of research using electronic medical records (EMR) systems, a
consideration is the need to derive structured data from free text, using
natural language processing (NLP) software.
However, not all individuals and/or institutions may have the necessary
resources to perform bulk NLP as they wish. In particular, there are two common
constraints. Firstly, the computing power necessary for NLP can be
considerable. Secondly, some NLP programs may be sensitive (for example, by
virtue of containing fragments of clinical free text from another institution,
used as an exemplar for an algorithm) and thus not widely distributable.
Accordingly, there is a need for a client–server NLP framework, and thus for a
defined request protocol.
The protocol is not RESTful_. In particular, it is not stateless.
State can be maintained on the server in between requests, if desired, through
the notion of queued requests.
Communications stack
~~~~~~~~~~~~~~~~~~~~
The underlying application layer is HTTP_ (and HTTPS, encrypted HTTP, is
strongly encouraged), over TCP/IP.
Request
~~~~~~~
Request format
^^^^^^^^^^^^^^
**Requests via HTTP POST**
Requests are always transmitted using the HTTP ``POST`` method.
- Rationale:
(1) Some calls modify state on the server, making ``GET`` inappropriate.
(2) Some requests will be large, making ``GET`` URL encoding inappropriate.
(3) Many requests involve sensitive data, which should not be encoded into
URLs via ``GET``.
(4) Therefore, all requests are via ``POST`` [#getvspost]_.
- The POST method itself is broad [#rfc7231]_.
**Requests in JSON**
The request content is in JSON_, with media type ``application/json``. The
encoding can be specified, and will be assumed to be UTF-8 if not specified.
- Rationale: we need a structured notation supporting list types, key–value
pair (KVP) types, a null notation, and arbitrary nesting. JSON fulfils this,
is simple (and thus fast), is legible to humans, and is widely supported
under many programming languages. Other formats such as XML require
considerably more complex parsing and are slower [#soap]_.
- Consideration: denial-of-service attacks in which large quantities of
nonsense are sent. We considered using XML instead of JSON as XML is
intrinsically ordered; we could enforce a constraint of having call arguments
such as parameter lists preceding textual content, and abandoning processing
if the request is malformed. Instead, we elected to keep JSON but move
authentication to the HTTP level (so non-authenticated requests can be thrown
away earlier) and allow the server to impose its own choice of maximum
request size. With that done, all requests coming through will be from
authenticated users and of a reasonable request size. At that point, JSON
continues to look simpler.
Note the JSON terminology:
- *Value:* one of:
- *String:* zero or more Unicode characters, wrapped in double quotes, using
backslash escapes.
- *Number:* a raw number.
- The literals ``true``, ``false``, or ``null``.
- An *object* or *array*, as below.
- *Object:* an unordered collection of comma-separated KVPs bounded by braces,
such as ``{key1: value1, key2: value2}``, where the keys are strings. Roughly
equivalent to a Python dictionary.
- *Array:* an ordered collection of comma-separated values bounded by square
brackets, such as ``[value1, value2]``. Equivalent to a Python list.
- JSON does *not* in general permit trailing commas in objects and arrays.
- Comments are not supported in JSON (but we may illustrate some JSON examples
here using ``#`` to indicate comments).
Where versions are passed, they are in `Semantic Versioning`_ 2.0.0
format. Semantic versions are strings using a particular format
(e.g. ``"1.2.0"``), referred to as a Version henceforth.
Where date/time values are passed, they are in `ISO-8601`_ format
and must include all three of: date, time, timezone. (The choice of timezone is
immaterial; servers may choose to use UTC_ throughout.)
Authentication at HTTP/HTTPS level
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Servers are free to require an authentication_ method using a standard HTTP
mechanism, such as `HTTP basic access authentication`_, `HTTP digest access
authentication`_, a `URL query string`_, or `OAuth`_. The mechanism for
doing so is not part of the API.
- It is expected that the HTTP front end would make the identity of an
authenticated user available to the NLPRP server, e.g. so the server can
check that a user is `authorized `_ for a specific NLP
processor or to impose volume/rate limits, but the mechanism for doing so is
not part of the API specification.
Compression at HTTP/HTTPS level
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Clients may compress requests by setting the HTTP header ``Content-Encoding:
gzip`` (see `HTTP Content-Encoding`_) and compressing the POST body
accordingly. Servers should accept requests compressed with ``gzip``.
- If the client sets the ``Accept-Encoding`` header (see `HTTP
Accept-Encoding`_), the server may return a suitably compressed response
(indicated via the ``Content-Encoding`` header in its reply).
Rejection of unauthorized or malformed responses
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Servers may reject invalid responses with an HTTP error. Typical reasons
might include failed authentication_ or authorization_; overly large
requests; requests that exceed a user's quota; syntactically invalid NLPRP
requests; syntactically valid requests that are invalid for this server (such
as requests that include invalid processors).
- Clients must accept HTTP errors either with a NLPRP response or without.
- If the body of the server's reply includes valid JSON where
``json_object["protocol"]["name"] == "nlprp"``, it is an NLPRP reply.
- If an error is returned via the NLP protocol, the ``status`` field in the
response_ must match the HTTP status code.
- The rationale for this is to reduce the effect of denial-of-service attacks
by preprocessing HTTP requests without the need to parse the NLPRP request
content, and to allow NLPRP server software to operate within a broader
institutional authentication, authorization, and/or accounting framework.
Request JSON structure
^^^^^^^^^^^^^^^^^^^^^^
The top-level structure of a request is a JSON object with the following keys.
.. rst-class:: nlprprequest
.. list-table::
:widths: 15 15 15 55
:header-rows: 1
:class: compact-table
* - Key
- JSON type
- Required?
- Description
* - ``protocol``
- Object
- Mandatory
- Details of the NLPRP protocol that the client is using, with keys:
- ``name`` (string): Must be ``"nlprp"``. Case insensitive.
- ``version`` (string): The Version of the NLPRP protocol that the
client is using.
* - ``command``
- String
- Mandatory
- NLPRP command, as below.
* - ``args``
- Value
- Optional
- Arguments to the command.
JSON does not care about whitespace in formatting, and neither the client nor
the server are under any obligation as to how they format their JSON.
.. _nlprp_response:
Response
~~~~~~~~
Response format
^^^^^^^^^^^^^^^
The request is returned over HTTP as media type ``application/json``. The
encoding *should* be specified (e.g. ``application/json; charset=utf-8``, and
will be assumed to be UTF-8 if not specified.
Response JSON structure
^^^^^^^^^^^^^^^^^^^^^^^
The top-level structure of a response is a JSON object with the following keys.
.. rst-class:: nlprpresponse
.. list-table::
:widths: 15 15 15 55
:header-rows: 1
:class: compact-table
* - Key
- JSON type
- Required?
- Description
* - ``status``
- Value
- Mandatory
- An integer matching the HTTP status code. Will be in the range [200,
299] for success.
* - ``errors``
- Array
- Optional
- If the status is not 102 or in the range [200, 299], one or more errors
will be given. Each error is an object with at least the following
keys:
- ``code`` (integer or null): error code
- ``message`` (string): brief textual description of the error
- ``description`` (string): more detail
* - ``protocol``
- Object
- Mandatory
- Details of the NLPRP protocol that the server is using. Keys:
- ``name`` (string): Must be ``"nlprp"``. Case insensitive.
- ``version`` (string): The Version of the NLPRP protocol that the
client is using.
* - ``server_info``
- Object
- Mandatory
- Details of the NLPRP server. Keys:
- ``name`` (string): Name of the NLPRP server software in use.
- ``version`` (string): The Version of the NLPRP server software.
NLPRP commands
~~~~~~~~~~~~~~
.. _nlprp_list_processors:
list_processors
^^^^^^^^^^^^^^^
No additional parameters are required, but there is an optional parameter.
.. rst-class:: nlprprequest
.. list-table::
:widths: 15 15 15 55
:header-rows: 1
:class: compact-table
* - Key
- JSON type
- Required?
- Description
* - ``sql_dialect``
- String
- Optional
- The SQL dialect that the client would prefer to receive its column
information in. (The server does not have to honour this.) See
:ref:`SQL dialects ` below.
*[Version 0.2.0 and higher.]*
This command lists the NLP processors available to the requestor. (This might
be a subset of all NLP processors on the server, depending on the
authentication and the permissions granted by the server.)
The relevant part of the response is:
.. rst-class:: nlprpresponse
.. list-table::
:widths: 15 15 15 55
:header-rows: 1
:class: compact-table
* - Key
- JSON type
- Required?
- Description
* - ``processors``
- Array
- Mandatory
- An array of objects. Each object has the following keys:
- ``name`` (string): the server’s name for the processor.
- ``title`` (string): generally, the processor’s name for itself.
- ``version`` (string): the Version of the processor.
- ``is_default_version`` (Boolean): indicates that this processor is
the default version for the given name. May be ``true`` for zero or
one versions for a given processor name.
- ``description`` (string): a description of the processor.
- ``schema_type`` (string): optional; must be one of ``unknown``,
``tabular``; default is ``unknown``. (Future versions may add more
schema types.)
*[Version 0.2.0 and higher.]*
- ``sql_dialect`` (string): the SQL dialect (see below) used within the
``tabular_schema`` object (see below). Must be present if
``tabular_schema`` is given.
*[Version 0.2.0 and higher.]*
- ``tabular_schema`` (object): an object that is present if and only if
``schema_type`` is ``tabular``. Represents a tabular schema and
describing the tables/columns provided by this processor. The format
of ``tabular_schema`` is described below.
*[Version 0.2.0 and higher.]*
.. _nlprp_schema_definition:
**Schema definition**
The NLP server may not know the output format of its NLP processors, in which
case ``schema_type`` may be set to ``unknown``. However, this is undesirable.
Processors *should* describe their schema by enumerating their output
tables/columns if they provide output compatible with storage in database
tables. To do so, the server sets ``schema_type`` to ``tabular`` and provides
the ``tabular_schema`` object.
The ``tabular_schema`` object defines a tabular schema. It maps *table names*
to *arrays of column definition objects*. Thus, in pseudocode:
.. rst-class:: nlprpresponse
.. code-block:: none
"tabular_schema": {
"table_name_1" : [
,
,
...
],
"table_name_2" : [
,
,
...
],
...
}
Most NLP processors produce output for a single database table. The table name
may be an empty string, ``""``, and that is fine (it is, after all, up to the
client to decide how it names its tables). Such a schema would look like this:
.. rst-class:: nlprpresponse
.. code-block:: none
"tabular_schema": {
"" : [
,
,
...
]
}
A few (e.g. GATE) processors may give output that requires more than one
database table to store. For example, a "people and places" processor may
return one kind of result when it finds a person, and another kind when it
finds a place; it would therefore need to define two tables.
Each column definition object describes a column in the database being used to
store results, and has the following keys.
.. rst-class:: nlprpresponse
.. list-table::
:header-rows: 1
:class: compact-table
* - Key
- JSON type
- Required?
- Description
* - ``column_name``
- String
- Mandatory
- Name of the column.
* - ``column_type``
- String
- Mandatory
- Full column data type, e.g. ``VARCHAR(64)``. (*)
* - ``data_type``
- String
- Mandatory
- Type name only, e.g. ``VARCHAR``
* - ``is_nullable``
- Boolean
- Mandatory
- Whether this column can contain ``null`` values.
* - ``column_comment``
- String or ``null``
- Optional
- Comment describing this column. (*)
* - ``data_type``
- String
- Mandatory
- Type name only, e.g. ``VARCHAR``
(The system follows the `ANSI SQL`_ INFORMATION_SCHEMA_ standard loosely;
specifically, using some of the columns found in
``INFORMATION_SCHEMA.COLUMNS``.)
For examples of ``INFORMATION_SCHEMA.COLUMNS``, see e.g.
- https://docs.microsoft.com/en-us/sql/relational-databases/system-information-schema-views/columns-transact-sql?view=sql-server-2017
- https://www.postgresql.org/docs/current/infoschema-columns.html
- https://dev.mysql.com/doc/refman/5.7/en/columns-table.html
(*) Not part of the ANSI standard; a MySQL extension to
``INFORMATION_SCHEMA.COLUMNS``.
.. _nlprp_sql_dialect:
**SQL dialects**
The ``sql_dialect`` parameter, detailed above, names the SQL dialect in which
``column_type`` and ``data_type`` are expressed. For example, "unlimited-length
text" might be ``VARCHAR(MAX)`` in the ``mssql`` dialect but ``LONGTEXT`` in
the ``mysql`` dialect.
SQL dialect values are strings representing those major dialects used by
SQLAlchemy_ (see https://docs.sqlalchemy.org/en/13/dialects/), i.e.
=============== ===============================================================
Dialect name Dialect
=============== ===============================================================
``mysql`` MySQL_
``mssql`` `Microsoft SQL Server`_
``oracle`` Oracle_
``postgresql`` PostgreSQL_
``sqlite`` SQLite_
=============== ===============================================================
*Request example*
A full request as sent over TCP/IP might be as follows, being sent to
``https://myserver.mydomain/nlp``:
.. rst-class:: nlprprequest
.. code-block:: none
POST /nlp HTTP/1.1
Host: myserver.mydomain
Content-Type: application/json; charset=utf-8
Content-Length:
{
"protocol": {
"name": "nlprp",
"version": "0.2.0"
},
"command": "list_processors"
}
*Response example*
For the specimen request above, the reply sent over TCP/IP might look like
this:
.. rst-class:: nlprpresponse
.. code-block:: none
HTTP/1.1 200 OK
Date: Mon, 13 Nov 2017 09:50:59 GMT
Server: Apache/2.4.23 (Ubuntu)
Content-Type: application/json; charset=utf-8
Content-Length:
{
"status": 200,
"protocol": {
"name": "nlprp",
"version": "0.2.0"
},
"server_info": {
"name": "My NLPRP server software",
"version": "0.2.0"
},
"processors": [
{
"name": "gate_medication",
"title": "SLAM BRC GATE-based medication finder",
"version": "1.2.0",
"is_default_version": true,
"description": "Finds drug names",
"schema_type": "unknown",
},
{
"name": "python_c_reactive_protein",
"title": "Cardinal RN (2017) CRATE CRP finder",
"version": "0.1.3",
"is_default_version": true,
"description": "Finds C-reactive protein (CRP) values",
"schema_type": "tabular",
"sql_dialect": "mysql",
"tabular_schema": {
"": [
{
"column_comment": "Variable name",
"column_name": "variable_name",
"column_type": "VARCHAR(64)",
"data_type": "VARCHAR",
"is_nullable": true
},
{
"column_comment": "Matching text contents",
"column_name": "_content",
"column_type": "TEXT",
"data_type": "TEXT",
"is_nullable": true
},
{
"column_comment": "Start position (of matching string within whole text)",
"column_name": "_start",
"column_type": "INTEGER",
"data_type": "INTEGER",
"is_nullable": true
},
{
"column_comment": "End position (of matching string within whole text)",
"column_name": "_end",
"column_type": "INTEGER",
"data_type": "INTEGER",
"is_nullable": true
},
{
"column_comment": "Text that matched the variable name",
"column_name": "variable_text",
"column_type": "TEXT",
"data_type": "TEXT",
"is_nullable": true
},
{
"column_comment": "Text that matched the mathematical relationship between variable and value (e.g. '=', '<=', 'less than')",
"column_name": "relation_text",
"column_type": "VARCHAR(50)",
"data_type": "VARCHAR",
"is_nullable": true
},
{
"column_comment": "Standardized mathematical relationship between variable and value (e.g. '=', '<=')",
"column_name": "relation",
"column_type": "VARCHAR(2)",
"data_type": "VARCHAR",
"is_nullable": true
},
{
"column_comment": "Matched numerical value, as text",
"column_name": "value_text",
"column_type": "TEXT",
"data_type": "TEXT",
"is_nullable": true
},
{
"column_comment": "Matched units, as text",
"column_name": "units",
"column_type": "VARCHAR(50)",
"data_type": "VARCHAR",
"is_nullable": true
},
{
"column_comment": "Numerical value in preferred units, if known",
"column_name": "value_mg_l",
"column_type": "FLOAT",
"data_type": "FLOAT",
"is_nullable": true
},
{
"column_comment": "Tense text, if known (e.g. 'is', 'was')",
"column_name": "tense_text",
"column_type": "VARCHAR(50)",
"data_type": "VARCHAR",
"is_nullable": true
},
{
"column_comment": "Calculated tense, if known (e.g. 'past', 'present')",
"column_name": "tense",
"column_type": "VARCHAR(7)",
"data_type": "VARCHAR",
"is_nullable": true
}
]
}
}
]
}
.. _nlprp_process:
process
^^^^^^^
This command is the central NLP processing request. The important detail is
passed in the top-level ``args`` parameter, where ``args`` is an object with
the following structure:
.. rst-class:: nlprprequest
.. list-table::
:widths: 15 15 15 55
:header-rows: 1
:class: compact-table
* - Key
- JSON type
- Required?
- Description
* - ``processors``
- Array
- Mandatory
- An array of objects, each with the following keys:
- ``name`` (string): the name of an NLP processor to apply to the text
(matching one of the names given by the server via the
list_processors_ command).
- ``version`` (optional string): the version of the named NLP processor
to use. If a version is not specified explicitly, and there is a
default version (see list_processors_), the server will use that.
- ``args``: optional key whose value is a JSON value considered to be
arguments to the processor (for future expansion).
* - ``queue``
- Boolean value (``true`` or ``false``)
- Optional (default ``false``)
- Controls queueing behaviour:
- If ``true``, adds the request to the server’s processing queue, and
returns a response giving queue information, or refuses the request.
See the show_queue_ and fetch_from_queue_ commands below.
- If ``false``, performs NLP immediately and returns the processing
result.
(Note, however, that the server can refuse to serve either immediate or
delayed results depending on its preference.)
* - ``client_job_id``
- String, of maximum length 150 characters
- Optional (if absent, an empty string will be used)
- This is for queued processing. It is a string that the server will
store alongside the queue request, to aid the client in identifying
requests belonging to the same job (if it splits work across many
requests). It is returned by the show_queue_ and fetch_from_queue_
commands.
* - ``include_text``
- Boolean value (``true`` or ``false``)
- Optional (default ``false``)
- If ``true``, includes the source text in the reply.
* - ``content``
- Array
- Mandatory
- A list of JSON objects representing text to be parsed (documents), with
optional associated metadata. Each object has the following keys:
- ``text`` (string, mandatory): The actual text to parse.
- ``metadata`` (value, optional): The metadata will be returned
verbatim with the results.
.. _nlprp_immediate_response:
**Immediate processing**
The response to a successful non-queued process command has the following
format (on top of the basic response structure):
.. rst-class:: nlprpresponse
.. list-table::
:widths: 15 15 15 55
:header-rows: 1
:class: compact-table
* - Key
- JSON type
- Required?
- Description
* - ``client_job_id``
- String
- Mandatory
- The same ``client_job_id`` as the client provided (or a blank string
if none was provided).
* - ``results``
- Array
- Mandatory
- An array of objects of the same length as ``content``, but in arbitrary
order, with each object having the following format:
- ``metadata`` (optional): a copy of the text-specific ``metadata``
provided in the request
- ``text`` (string, optional); if ``include_text`` was true, the source
text is included here.
- ``processors``: array of objects in the same order as the
``processors`` parameter in the request, and whose keys are:
- ``name`` (string): name of the processor (as per
list_processors_)
- ``title`` (string): title of the processor (as per
list_processors_)
- ``version`` (string): Version of the processor (as per
list_processors_)
- ``success`` (Boolean): ``true`` for success, ``false`` for failure.
This allows for the possibility of text-specific failure, e.g. a
document that crashes the NLP parser or otherwise fails
dynamically.
- ``errors`` (Array, optional): if ``success`` is ``false``,
this should be present and describe the reason(s) for failure. It
is an array of error objects, where each error is an object with at
least the following keys:
- ``code`` (integer or null): error code
- ``message`` (string): brief textual description of the error
- ``description`` (string): more detail
- ``results``: see :ref:`Format of per-processor results
` below.
Note that it is strongly advisable for clients to specify ``metadata``
as this will be necessary for them to recover order information
whenever ``content`` has more than one item.
.. _nlprp_format_of_per_processor_results:
**Format of per-processor results**
Remember that a single piece of source text can generate zero, one, or many NLP
matches from each processor; and that a single NLP “match” can involve highly
structured results, but typically involves one set of key/value pairs.
We now consider the
``response["results"][result_num]["processors"][processor_num]["results"]``
value. This is processor-specific.
- For a failed request, this should be an empty array, ``[]``, or an empty
object, ``{}``. (Note that it may also be empty following success, meaning
that the processor found nothing of interest to it.)
- If the processor does not offer a ``tabular_schema`` definition (see
:ref:`Schema definition ` above), then the format of
``results`` is not constrained.
*[In NLPRP Version 0.1.0, there were no constraints, as a result.]*
A common format is an array of objects (each object providing a key-value
mapping of column/field names to values), like this:
.. rst-class:: nlprpresponse
.. code-block:: none
"results": [
{
# row 1
"column_name_1": ,
"column_name_2": ,
...
},
{
# row 2
"column_name_1": ,
"column_name_2": ,
...
},
...
]
- If the processor does offer a ``tabular_schema`` definition, and that
definition is for only a single table, then ``results`` *may* be an array of
objects, each object representing a database row and containing a mapping
from column names to values (exactly as above).
- If the processor offers a ``tabular_schema`` definition, the other
permissible format is that ``results`` is an object, not an array. In this
case, the ``results`` object maps table names (exactly as in the schema) to
arrays of rows, like this:
.. rst-class:: nlprpresponse
.. code-block:: none
"results": {
"table_name_1": [
{
# row 1
"column_name_1": ,
"column_name_2": ,
...
},
{
# row 2
"column_name_1": ,
"column_name_2": ,
...
},
...
],
"table_name_2": [
{
# row 1
"column_name_3": ,
"column_name_4": ,
...
},
{
# row 2
"column_name_3": ,
"column_name_4": ,
...
},
...
]
}
This format *must* be used by processors providing a ``tabular_schema`` and
using more than one table.
- It is an error for the processor to offer a ``tabular_schema`` definition and
then not abide by it (e.g. by providing table or column names not as
described in the schema, or by returning data in non-tabular format).
**Example**
An example exchange using immediate processing follows. The request sends three
pieces of text with metadata, and requests two processors to be run on each of
them. (Neither processor takes any arguments. Since no version is specified for
the ``python_c_reactive_protein`` processor, the default version will be used.)
.. rst-class:: nlprprequest
.. code-block:: none
POST /nlp HTTP/1.1
Host: myserver.mydomain
Content-Type: application/json; charset=utf-8
Content-Length:
{
"protocol": {
"name": "nlprp",
"version": "0.2.0"
},
"command": "process",
"args": {
"processors": [
{
"name": "gate_medication",
"version": "1.2.0",
},
{
"name": "python_c_reactive_protein",
},
],
"queue": false,
"client_job_id": "My NLP job 57 for depression/CRP",
"include_text": false,
"content": [
{
"metadata": {"myfield": "progress_notes", "pk": 12345},
"text": "My old man’s a dustman. He wears a dustman’s hat."
},
{
"metadata": {"myfield": "progress_notes", "pk": 23456},
"text": "Dr Bloggs started aripiprazole 5mg od today."
},
{
"metadata": {"myfield": "clinical_docs", "pk": 777},
"text": "CRP 45; concern about UTI. No longer on prednisolone. Has started co-amoxiclav 625mg tds."
}
]
}
}
Here’s the response. The first piece of text generates no hits for either
processor. The second generates a hit for the ‘medication’ processor. The third
generates a hit for ‘CRP’ and two drugs.
.. rst-class:: nlprpresponse
.. code-block:: none
HTTP/1.1 200 OK
Date: Mon, 13 Nov 2017 09:50:59 GMT
Server: Apache/2.4.23 (Ubuntu)
Content-Type: application/json; charset=utf-8
Content-Length:
{
"status": 200,
"protocol": {
"name": "nlprp",
"version": "0.2.0"
},
"server_info": {
"name": "My NLPRP server software",
"version": "0.2.0"
},
"client_job_id": "My NLP job 57 for depression/CRP",
"results": [
{
"metadata": {"myfield": "progress_notes", "pk": 12345},
"processors": [
{
"name": "gate_medication",
"title": "SLAM BRC GATE-based medication finder",
"version": "1.2.0",
"success": true,
"results": []
},
{
"name": "python_c_reactive_protein",
"title": "Cardinal RN (2017) CRATE CRP finder",
"version": "0.1.3",
"success": true,
"results": []
},
]
},
{
"metadata": {"myfield": "progress_notes", "pk": 23456},
"processors": [
{
"name": "gate_medication",
"title": "SLAM BRC GATE-based medication finder",
"version": "1.2.0",
"success": true,
"results": [
{
"drug": "aripiprazole",
"drug_type": "BNF_generic",
"dose": "5mg",
"dose_value": 5,
"dose_unit": "mg",
"dose_multiple": 1,
"route": null,
"status": "start",
"tense": "present"
}
]
},
{
"name": "python_c_reactive_protein",
"title": "Cardinal RN (2017) CRATE CRP finder",
"version": "0.1.3",
"success": true,
"results": []
},
]
},
{
"metadata": {"myfield": "clinical_docs", "pk": 777},
"processors": [
{
"name": "gate_medication",
"title": "SLAM BRC GATE-based medication finder",
"version": "1.2.0",
"results": [
{
"drug": "prednisolone",
"drug_type": "BNF_generic",
"dose": null,
"dose_value": null,
"dose_unit": null,
"dose_multiple": null,
"route": null,
"status": "stop",
"tense": null
},
{
"drug": "co-amoxiclav",
"drug_type": "BNF_generic",
"dose": "625mg",
"dose_value": 625,
"dose_unit": "mg",
"dose_multiple": 1,
"route": "po",
"status": "start",
"tense": "present"
}
]
},
{
"name": "python_c_reactive_protein",
"title": "Cardinal RN (2017) CRATE CRP finder",
"version": "0.1.3",
"results": [
{
"startpos": 1,
"endpos": 7,
"variable_name": "CRP",
"variable_text": "CRP",
"relation": "",
"value_text": "45",
"units": "",
"value_mg_l": 45,
"tense_text": "",
"tense": "present"
}
]
},
]
}
]
}
Note that the two NLP processors are returning different sets of information,
in a processor-specific way.
**Queued processing**
NLP can be slow. Non-queued commands require that the server performs all the
NLP requested within the HTTP timeout period, which may not be feasible;
therefore, the protocol supports queuing. With a queued process request, the
server takes the data, says “thanks, I’m thinking about it”, and the client can
check back later. When the client checks back, the server might have data to
offer it or may still be busy.
One risk of queued commands is to the server: clients may send NLP requests
faster than the server can handle them. Therefore, the protocol allows the
server to refuse queued requests.
Another thing to note is that immediate requests may or may not require the raw
text to “touch down” somewhere on the server — what the server does is up to it
— but typically, “immediate” requests require minimal (e.g. in-memory) storage
of the raw text, whilst “queued” requests inevitably require that the server
store the text (e.g. on disk, perhaps in a database) for the lifetime of the
queue request.
**Initial successful response to process command with queued = true**
The initial response has an HTTP status code of 202 (Accepted) and a top-level
key of ``queue_id``, whose value is a string. Like this:
.. rst-class:: nlprpresponse
.. code-block:: none
HTTP/1.1 202 Accepted
Date: Mon, 13 Nov 2017 09:50:59 GMT
Server: Apache/2.4.23 (Ubuntu)
Content-Type: application/json; charset=utf-8
Content-Length:
{
"status": 202,
"protocol": {
"name": "nlprp",
"version": "0.1.0"
},
"server_info": {
"name": "My NLPRP server software",
"version": "0.1.0"
},
"queue_id": "7586876b-49cb-447b-9db3-b640e02f4f9b"
}
.. _nlprp_show_queue:
show_queue
^^^^^^^^^^
The ``show_queue`` command allows the client to view its queue status. It has
one optional argument:
.. rst-class:: nlprprequest
.. list-table::
:widths: 15 15 15 55
:header-rows: 1
:class: compact-table
* - Key
- JSON type
- Required?
- Description
* - ``client_job_id``
- String
- Optional
- An optional client job ID (see process_). If absent, all queue entries
for this client are shown. If present, only queue entries for the
specified ``client_job_id`` are shown.
The reply contains this extra information:
.. rst-class:: nlprpresponse
.. list-table::
:widths: 15 15 15 55
:header-rows: 1
:class: compact-table
* - Key
- JSON type
- Required?
- Description
* - ``queue``
- Array
- Mandatory
- An array of objects, one for each incomplete queue entry, each with the
following keys/values:
- ``queue_id``: queue ID, as returned from the process_ command
- ``client_job_id``: the client's job ID (see process_).
- ``status``: a string; one of: ``ready``, ``busy``.
- ``datetime_submitted``: date/time submitted, in ISO-8601 format.
- ``datetime_completed``: date/time completed, in ISO-8601 format, or
``null`` if it’s not yet complete.
Specimen request:
.. rst-class:: nlprprequest
.. code-block:: none
POST /nlp HTTP/1.1
Host: myserver.mydomain
Content-Type: application/json; charset=utf-8
Content-Length:
{
"protocol": {
"name": "nlprp",
"version": "0.1.0"
},
"command": "show_queue"
}
and corresponding response:
.. rst-class:: nlprpresponse
.. code-block:: none
HTTP/1.1 200 OK
Date: Mon, 13 Nov 2017 09:50:59 GMT
Server: Apache/2.4.23 (Ubuntu)
Content-Type: application/json; charset=utf-8
Content-Length:
{
"status": 200,
"protocol": {
"name": "nlprp",
"version": "0.1.0"
},
"server_info": {
"name": "My NLPRP server software",
"version": "0.1.0"
},
"queue": [
{
"queue_id": "7586876b-49cb-447b-9db3-b640e02f4f9b",
"client_job_id": "My NLP job 57 for depression/CRP",
"status": "ready",
"datetime_submitted": "2017-11-13T09:49:38.578474Z",
"datetime_completed": "2017-11-13T09:50:00.817611Z"
}
{
"queue_id": "6502b94a-2332-4f51-b2a3-337dc5d36ca0",
"client_job_id": "My NLP job 57 for depression/CRP",
"status": "busy",
"datetime_submitted": "2017-11-13T09:49:39.717170Z",
"datetime_completed": null
}
]
}
.. _nlprp_fetch_from_queue:
fetch_from_queue
^^^^^^^^^^^^^^^^
Fetches a single entry from the queue, if it exists and is ready for
collection. The top-level ``args`` should contain a key ``queue_id`` containing
the queue ID.
- If the queue ID doesn’t correspond to a current queue entry, an error will be
returned (HTTP 404 Not Found).
- If the queue entry is ready for collection, the reply will be of the format
for an “immediate” process request. The queue entry will be deleted upon
collection.
- If the queue entry is still busy being processed, an information code will be
returned (HTTP 202 Accepted), along with details as follows:
.. rst-class:: nlprpresponse
.. list-table::
:widths: 15 15 15 55
:header-rows: 1
:class: compact-table
* - Key
- JSON type
- Required?
- Description
* - ``n_docprocs``
- Value
- Optional
- The total number of document/processor pairs corresponding to the
queue entry.
* - ``n_docprocs_completed``
- Value
- Optional
- The number of document/processor pairs corresponding to the queue
entry for which processing has been completed.
.. _nlprp_delete_from_queue:
delete_from_queue
^^^^^^^^^^^^^^^^^
For this command, the top-level ``args`` should be an object with the following
keys:
.. rst-class:: nlprprequest
.. list-table::
:widths: 15 15 15 55
:header-rows: 1
:class: compact-table
* - Key
- JSON type
- Required?
- Description
* - ``queue_ids``
- Array
- Optional
- An array of strings, each representing a queue ID to be deleted.
* - ``client_job_ids``
- Array
- Optional
- An array of strings, each representing a client job ID for which all
queue IDs should be deleted.
* - ``delete_all``
- Boolean value (``true`` or ``false``)
- Optional (default ``false``)
- If true, all queue entries (for this client!) are deleted.
Specimen Python 3.7+ client program
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Very briefly, run ``pip install requests crate_anon``, and then you can run
this:
.. rst-class:: nlprprequest
.. literalinclude:: _nlprp_test_client.py
:language: python
Specimen Python 3.7+ server program
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Similarly, for a dummy server program, run ``pip install pyramid crate_anon``
and then you can run this:
.. rst-class:: nlprpresponse
.. literalinclude:: _nlprp_test_server.py
:language: python
More on error responses
~~~~~~~~~~~~~~~~~~~~~~~
The main design question here is whether HTTP status codes should be used for
errors, or not. There are pros and cons here [#errorsviahttpstatus]_. We shall
follow best practice and encode the status both in HTTP and in the JSON.
Specific HTTP status codes not detailed above include:
================== ========================================= ==========================
Command Situation HTTP status code
================== ========================================= ==========================
Any Success 200 OK
Any Request malformed 400 Bad Request
Any Authorization failed 401 Unauthorized
Any Server bug 500 Internal Server Error
process_ Results returned 200 OK
process_ Request queued 202 Accepted
process_ Upstream server went wrong 502 Bad Gateway
process_ Server is too busy right now 503 Service Unavailable
fetch_from_queue_ No such queue entry 404 Not Found
fetch_from_queue_ Entry still in queue and being processed 202 Accepted
================== ========================================= ==========================
Python internal NLP interface
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The NLPRP server should manage per-text metadata (from the process_ command)
internally. We define a very generic Python interface for the NLPRP server to
request NLP results from a specific Python NLP processor:
.. rst-class:: nlprpresponse
.. code-block:: python
def nlp_process(text: str,
processor_args: Any = None) -> List[Dict[str, Any]]:
"""
Standardized interface via the NLP Request Protocol (NLPRP).
Processes text using some form of natural language processing (NLP).
Args:
text: the text to process
processor_args: additional arguments supplied by the user [via a
json.loads() call upon the processor argument value].
Returns:
a list of dictionaries with string keys, suitable for conversion to
JSON using a process such as:
.. code-block:: python
import json
from my_nlp_module import nlp_process
result_dict = nlp_process("some text")
result_json = json.dumps(result_dict)
"""
raise NotImplementedError()
The combination of this standard interface plus the Python Package Index (PyPI)
should allow easy installation of Python NLP managers (by Python package name
and version). The NLPRP server should be able to import a ``nlp_process`` or
equivalent function from the top-level package.
Existing code of relevance
~~~~~~~~~~~~~~~~~~~~~~~~~~
The CRATE toolchain has Python handlers for firing up external NLP processors
including GATE and other Java-based tools, and piping text to them; similarly
for its internal Python code. From the Cambridge perspective we are likely to
extend and use CRATE to send data to the NLP API/service and manage results,
but it is also potentially extensible to serve as the NLP API server.
Aspects of server function that are not part of the NLPRP specification
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The following are implementation details that are at the server's discretion:
- authentication_
- authorization_
- accounting (logging, billing, size/frequency restrictions)
- containerization, parallel processing, message queue details
Abbreviations used in this section
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
======= =======================================================================
EMR electronic medical records
HTTP hypertext transport protocol
HTTPS secure HTTP
IP Internet protocol
ISO International Organization for Standardization
JSON JavaScript Object Notation
KVP key–value pair
NHS UK National Health Service
NLP natural language processing
NLPRP NLP Request Protocol
PyPI The Python Package Index; https://pypi.python.org/
REST Representational state transfer
TCP transmission control protocol
UK United Kingdom
URL uniform resource locator
UTC Universal Coordinated Time
UTF-8 Unicode Transformation Format, 8-bit
XML Extensible Markup Language
======= =======================================================================
NLPRP things to do and potential future requirements
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Corpus (supra-document) processing:
- There may be future use cases where the NLP processor must simultaneously
consider more than one document (a "corpus" of documents, in GATE_
terminology). This is not currently supported. However, batch processing is
currently supported.
NLPRP history
~~~~~~~~~~~~~
**v0.0.1**
- Started 13 Nov 2017; Rudolf Cardinal.
**v0.0.2**
- RNC
- Minor changes 18 July 2018 following discussion with SLAM/KCL team.
**v0.1.0**
- Amendments 4 Oct 2018, RNC/IR/FS/JK/AR.
- Authentication moved out of the API.
- Authorization moved out of the API.
- The server may "fail" requests at the HTTP level or at the subsequent NLPRP
processing stage (i.e. failures may or may not include an NLPRP response
object).
- Compression at HTTP level discussed; servers should accept ``gzip``
compression from the client.
- Order of ``results`` object changed to arbitrary (to facilitate parallel
processing).
- ``echorequest``/``echo`` parameters removed; this was pointless as all HTTP
calls have an associated reply, so the client should never fail to know what
was echoed back.
- ``is_default_version`` argument to the list_processors_ reply, and
``version`` argument to process_.
- Comment re future potential use case for corpus-level processing
- Signalling mechanism for dynamic failure via the ``success`` and
``errors`` parameters to the response (see `immediate response
`_).
- Ability for the client to pass a ``client_job_id`` to
the queued processing mode, so it can add many requests to the same job and
retrieve this data as part of ``show_queue``. Similar argument to
delete_from_queue_.
- Consideration of processor version control and how this is managed in
practice (e.g. Python modules; GATE apps) isn't part of the API; removed
from NLPAPI "to-do" list.
**v0.2.0**
- 4-6 Aug 2019, RNC.
- Processors can describe their output format: ``schema_type`` and
``tabular_schema`` attribute in the response to the list_processors_ command,
with associated ``sql_dialect`` options.
- Corresponding constraints on the results format for processors that provide a
tabular schema.
**v0.3.0**
- 14 Feb 2023, RNC.
- Use HTTP 202 (Accepted), not 102 (Processing), for the in-progress response
to fetch_from_queue_, and report how many are complete. See
https://github.com/ucam-department-of-psychiatry/crate/issues/106.
===============================================================================
.. rubric:: Footnotes
.. [#getvspost]
http://blog.teamtreehouse.com/the-definitive-guide-to-get-vs-post
.. [#rfc7231]
https://tools.ietf.org/html/rfc7231#section-4.3.3
.. [#soap]
https://en.wikipedia.org/wiki/SOAP
.. [#errorsviahttpstatus]
See:
- https://stackoverflow.com/questions/942951/rest-api-error-return-good-practices
- https://cloud.google.com/storage/docs/json_api/v1/status-codes
- https://blogs.mulesoft.com/dev/api-dev/api-best-practices-response-handling/
- https://developer.twitter.com/en/docs/basics/response-codes
- http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
- https://blog.runscope.com/posts/6-common-api-errors