diff --git a/README.md b/README.md index beea794..37a8797 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ The remainder of the tutorials are optional and for users who wish to e.g. train 3. [Evaluate on GERBIL.](https://rel.readthedocs.io/en/latest/tutorials/evaluate_gerbil/) 4. [Deploy REL for a new Wikipedia corpus](https://rel.readthedocs.io/en/latest/tutorials/deploy_REL_new_wiki/): 5. [Reproducing our results](https://rel.readthedocs.io/en/latest/tutorials/reproducing_our_results/) -6. [REL as systemd service](https://rel.readthedocs.io/en/latest/tutorials/systemd_instructions/) +6. [REL server](https://rel.readthedocs.io/en/latest/tutorials/server/) 7. [Notes on using custom models](https://rel.readthedocs.io/en/latest/tutorials/custom_models/) ## REL variants diff --git a/docs/server_api.md b/docs/server_api.md new file mode 100644 index 0000000..160983c --- /dev/null +++ b/docs/server_api.md @@ -0,0 +1,221 @@ +# REL server API docs + +This page documents usage for the [REL server](https://rel.cs.ru.nl/docs). The live, up-to-date api can be found either [here](https://rel.cs.ru.nl/api/docs) or [here](https://rel.cs.ru.nl/api/redocs). + +Scroll down for code samples, example requests and responses. + +## Server status + +`GET /` + +Returns server status. + +### Example + +> Response + +```json +{ + "schemaVersion": 1, + "label": "status", + "message": "up", + "color": "green" +} +``` + +> Code + +```python +>>> import requests +>>> requests.get("https://rel.cs.ru.nl/api/").json() +{'schemaVersion': 1, 'label': 'status', 'message': 'up', 'color': 'green'} +``` + +## Named Entity Linking + +`POST /` +`POST /ne` + +Submit your text here for entity disambiguation or linking. + +The REL annotation mode can be selected by changing the path. +use `/` or `/ne` for annotating regular text with named +entities (default), `/ne_concept` for regular text with both concepts and +named entities, and `/conv` for conversations with both concepts and +named entities. + +> Schema + +`text` (string) +: Text for entity linking or disambiguation. + +`spans` (list) + +: For EL: the spans field needs to be set to an empty list. + +: For ED: spans should consist of a list of tuples, where each tuple refers to the start position (int) and length of a mention (int). + +: This is used when mentions are already identified and disambiguation is only needed. Each tuple represents start position and length of mention (in characters); e.g., [(0, 8), (15,11)] for mentions 'Nijmegen' and 'Netherlands' in text 'Nijmegen is in the Netherlands'. + +`tagger` (string) +: NER tagger to use. Must be one of `ner-fast`, `ner-fast-with-lowercase`. Default: `ner-fast`. + +### Example + +> Request body + +```json +{ + "text": "If you're going to try, go all the way - Charles Bukowski.", + "spans": [ + [ + 41, + 16 + ] + ], + "tagger": "ner-fast" +} +``` + +> Response + +The 7 values of the annotation represent the start index, end index, the annotated word, prediction, ED confidence, MD confidence, and tag. + +```json +[ + + [ + 41, + 16, + "Charles Bukowski", + "Charles_Bukowski", + 0, + 0, + "NULL" + ] + +] +``` + +> Code + +```python +>>> import requests +>>> myjson = { + "text": "REL is a modular Entity Linking package that can both be integrated in existing pipelines or be used as an API.", + "tagger": "ner-fast" +} +>>> requests.post("https://rel.cs.ru.nl/api/ne", json=myjson).json() +[[0, 3, 'REL', 'Category_of_relations', 0, 0, 'ORG'], [107, 3, 'API', 'Application_programming_interface', 0, 0, 'MISC']] +``` + +## Conversational entity linking + +`POST /conv` + +Submit your text here for conversational entity linking. + +> Schema + +`text` (list) +: Text is specified as a list of turns between two speakers. + + `speaker` (string) + : Speaker for this turn, must be one of `USER` or `SYSTEM` + + `utterance` (string) + : Input utterance to be annotated. + +`tagger` (string) +: NER tagger to use. Choices: `default`. + + +### Example + +> Request body + +```json +{ + "text": [ + { + "speaker": "USER", + "utterance": "I am allergic to tomatoes but we have a lot of famous Italian restaurants here in London." + }, + { + "speaker": "SYSTEM", + "utterance": "Some people are allergic to histamine in tomatoes." + }, + { + "speaker": "USER", + "utterance": "Talking of food, can you recommend me a restaurant in my city for our anniversary?" + } + ], + "tagger": "default" +} +``` + +> Response + +The 7 values of the annotation represent the start index, end index, the annotated word, prediction, ED confidence, MD confidence, and tag. + +```json +[ + { + "speaker": "USER", + "utterance": "I am allergic to tomatoes but we have a lot of famous Italian restaurants here in London.", + "annotations": [ + [17, 8, "tomatoes", "Tomato"], + [54, 19, "Italian restaurants", "Italian_cuisine"], + [82, 6, "London", "London"] + ] + }, + ... +] +``` + +> Code + +```python +>>> import requests +>>> myjson = { + "text": [...], + "tagger": "default" +} +>>> requests.post("https://rel.cs.ru.nl/api/conv", json=myjson).json() +[{...}] +``` + + +## Conceptual entity linking + +`POST /ne_concept` + +Submit your text here for conceptual entity disambiguation or linking. + +### Example + +> Request body + +```json +{} +``` + +> Response + +Not implemented. + +```json +{} +``` + +> Code + +```python +>>> import requests +>>> myjson = { + "text": ..., +} +>>> requests.post("https://rel.cs.ru.nl/api/ne_concept", json=myjson).json() +{...} +``` + diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index 371db1a..3aba1f0 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -12,6 +12,6 @@ The remainder of the tutorials are optional and for users who wish to e.g. train 3. [Evaluate on GERBIL.](evaluate_gerbil/) 4. [Deploy REL for a new Wikipedia corpus](deploy_REL_new_wiki/): 5. [Reproducing our results](reproducing_our_results/) -6. [REL as systemd service](systemd_instructions/) +6. [REL server](server/) 7. [Notes on using custom models](custom_models/) 7. [Conversational entity linking](conversations/) diff --git a/docs/tutorials/server.md b/docs/tutorials/server.md new file mode 100644 index 0000000..523a70f --- /dev/null +++ b/docs/tutorials/server.md @@ -0,0 +1,86 @@ +# REL server + +This section describes how to set up and use the REL server. + +## Running the server + +The server uses [fastapi](https://fastapi.tiangolo.com/) as the web framework. +FastAPI is a modern, fast (high-performance), web framework for building APIs bases on standard Python type hints. +When combined with [pydantic](https://docs.pydantic.dev/) this makes it very straightforward to set up a web API with minimal coding. + +```bash +python ./src/REL/server.py \ + $REL_BASE_URL \ + wiki_2019 \ + --ner-model ner-fast ner-fast-with-lowercase +``` + +This will open the API at the default `host`/`port`: . + +One of the advantage of using fastapi is its automated docs by adding `/docs` or `/redoc` to the end of the url: + +- +- + +You can use `python ./src/scripts/test_server.py` for some examples of the queries and to test the server. + +### Setup + +Set `$REL_BASE_URL` to the path where your data are stored (`base_url`). + +For mention detection and entity linking, the `base_url` must contain all the files specified [here](../how_to_get_started). + +In addition, for conversational entity linking, additonal files are needed as specified [here](../conversations) + +In summary, these paths must exist: + + - `$REL_BASE_URL/wiki_2019` or `$REL_BASE_URL/wiki_2014` + - `$REL_BASE_URL/bert_conv` for conversational EL) + - `$REL_BASE_URL/s2e_ast_onto ` for conversational EL) + +## Running REL as a systemd service + +In this tutorial we provide some instructions on how to run REL as a systemd +service. This is a fairly simple setup, and allows for e.g. automatic restarts +after crashes or machine reboots. + +### Create `rel.service` + +For a basic systemd service file for REL, put the following content into +`/etc/systemd/system/rel.service`: + +```ini +[Unit] +Description=My REL service + +[Service] +Type=simple +ExecStart=/bin/bash -c "python src/REL/server.py" +Restart=always + +[Install] +WantedBy=multi-user.target +``` + +Note that you may have to alter the code in `server.py` to reflect +necessary address/port changes. + +This is the simplest way to write a service file for REL; it could be more +complicated depending on any additional needs you may have. For further +instructions, see e.g. [here](https://wiki.debian.org/systemd/Services) or `man +5 systemd.service`. + +### Enable the service + +In order to enable the service, run the following commands in your shell: + +```bash +systemctl daemon-reload + +# For systemd >= 220: +systemctl enable --now rel.service + +# For earlier versions: +systemctl enable rel.service +reboot +``` diff --git a/docs/tutorials/systemd_instructions.md b/docs/tutorials/systemd_instructions.md deleted file mode 100644 index 3613e36..0000000 --- a/docs/tutorials/systemd_instructions.md +++ /dev/null @@ -1,46 +0,0 @@ -# Running REL as a systemd service - -In this tutorial we provide some instructions on how to run REL as a systemd -service. This is a fairly simple setup, and allows for e.g. automatic restarts -after crashes or machine reboots. - -## Create `rel.service` - -For a basic systemd service file for REL, put the following content into -`/etc/systemd/system/rel.service`: - -```ini -[Unit] -Description=My REL service - -[Service] -Type=simple -ExecStart=/bin/bash -c "python -m rel.scripts.code_tutorials.run_server" -Restart=always - -[Install] -WantedBy=multi-user.target -``` - -Note that you may have to alter the code in `run_server.py` to reflect -necessary address/port changes. - -This is the simplest way to write a service file for REL; it could be more -complicated depending on any additional needs you may have. For further -instructions, see e.g. [here](https://wiki.debian.org/systemd/Services) or `man -5 systemd.service`. - -## Enable the service - -In order to enable the service, run the following commands in your shell: - -```bash -systemctl daemon-reload - -# For systemd >= 220: -systemctl enable --now rel.service - -# For earlier versions: -systemctl enable rel.service -reboot -``` diff --git a/mkdocs.yml b/mkdocs.yml index 2a11e66..bf4ab28 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,13 +2,14 @@ site_name: "REL: Radboud Entity Linker" nav: - Home: index.md + - API: server_api.md - Tutorials: - tutorials/how_to_get_started.md - tutorials/e2e_entity_linking.md - tutorials/evaluate_gerbil.md - tutorials/deploy_REL_new_wiki.md - tutorials/reproducing_our_results.md - - tutorials/systemd_instructions.md + - tutorials/server.md - tutorials/custom_models.md - tutorials/conversations.md - Python API reference: diff --git a/scripts/code_tutorials/run_server.py b/scripts/code_tutorials/run_server.py deleted file mode 100644 index 701d88b..0000000 --- a/scripts/code_tutorials/run_server.py +++ /dev/null @@ -1,34 +0,0 @@ -from http.server import HTTPServer - -from REL.entity_disambiguation import EntityDisambiguation -from REL.ner import load_flair_ner -from REL.server import make_handler - -# 0. Set your project url, which is used as a reference for your datasets etc. -base_url = "" -wiki_version = "wiki_2019" - -# 1. Init model, where user can set his/her own config that will overwrite the default config. -# If mode is equal to 'eval', then the model_path should point to an existing model. -config = { - "mode": "eval", - "model_path": "{}/{}/generated/model".format(base_url, wiki_version), -} - -model = EntityDisambiguation(base_url, wiki_version, config) - -# 2. Create NER-tagger. -tagger_ner = load_flair_ner("ner-fast") # or another tagger - -# 3. Init server. -server_address = ("127.0.0.1", 5555) -server = HTTPServer( - server_address, - make_handler(base_url, wiki_version, model, tagger_ner), -) - -try: - print("Ready for listening.") - server.serve_forever() -except KeyboardInterrupt: - exit(0) diff --git a/scripts/code_tutorials/run_server_temp.py b/scripts/code_tutorials/run_server_temp.py deleted file mode 100644 index 26dd2d8..0000000 --- a/scripts/code_tutorials/run_server_temp.py +++ /dev/null @@ -1,72 +0,0 @@ -from http.server import HTTPServer - -# --------------------- Overwrite class -from typing import Dict - -import flair -import torch -import torch.nn -from flair.data import Dictionary as DDD -from flair.embeddings import TokenEmbeddings -from flair.models import SequenceTagger -from torch.nn.parameter import Parameter - -from REL.entity_disambiguation import EntityDisambiguation -from REL.ner import load_flair_ner -from REL.server import make_handler - - -def _init_initial_hidden_state(self, num_directions: int): - hs_initializer = torch.nn.init.xavier_normal_ - lstm_init_h = torch.nn.Parameter( - torch.zeros(self.rnn.num_layers * num_directions, self.hidden_size), - requires_grad=True, - ) - lstm_init_c = torch.nn.Parameter( - torch.zeros(self.rnn.num_layers * num_directions, self.hidden_size), - requires_grad=True, - ) - return hs_initializer, lstm_init_h, lstm_init_c - - -SequenceTagger._init_initial_hidden_state = _init_initial_hidden_state -# --------------------- - - -def user_func(text): - spans = [(0, 5), (17, 7), (50, 6)] - return spans - - -# 0. Set your project url, which is used as a reference for your datasets etc. -base_url = "/store/projects/REL" -wiki_version = "wiki_2019" - -# 1. Init model, where user can set his/her own config that will overwrite the default config. -# If mode is equal to 'eval', then the model_path should point to an existing model. -config = { - "mode": "eval", - "model_path": "{}/{}/generated/model".format(base_url, wiki_version), -} - -model = EntityDisambiguation(base_url, wiki_version, config) - -# 2. Create NER-tagger. -tagger_ner = load_flair_ner("ner-fast-with-lowercase") - -# 2.1. Alternatively, one can create his/her own NER-tagger that given a text, -# returns a list with spans (start_pos, length). -# tagger_ner = user_func - -# 3. Init server. -server_address = ("127.0.0.1", 1235) -server = HTTPServer( - server_address, - make_handler(base_url, wiki_version, model, tagger_ner), -) - -try: - print("Ready for listening.") - server.serve_forever() -except KeyboardInterrupt: - exit(0) diff --git a/scripts/test_server.py b/scripts/test_server.py index 2953248..2d63b3d 100644 --- a/scripts/test_server.py +++ b/scripts/test_server.py @@ -5,9 +5,9 @@ # # To run: # -# python .\src\REL\server.py $REL_BASE_URL wiki_2019 +# python .\src\REL\server.py $REL_BASE_URL wiki_2019 --ner-model ner-fast ner-fast-with-lowercase # or -# python .\src\REL\server.py $env:REL_BASE_URL wiki_2019 +# python .\src\REL\server.py $env:REL_BASE_URL wiki_2019 --ner-model ner-fast ner-fast-with-lowercase # # Set $REL_BASE_URL to where your data are stored (`base_url`) # @@ -19,44 +19,80 @@ # -host = 'localhost' -port = '5555' +host = "localhost" +port = "5555" -text1 = { - "text": "REL is a modular Entity Linking package that can both be integrated in existing pipelines or be used as an API.", - "spans": [] -} - -conv1 = { - "text" : [ - { - "speaker": - "USER", - "utterance": - "I am allergic to tomatoes but we have a lot of famous Italian restaurants here in London.", +items = ( + { + "endpoint": "", + "payload": { + "tagger": "ner-fast", + "text": "REL is a modular Entity Linking package that can both be integrated in existing pipelines or be used as an API.", + "spans": [], + }, + }, + { + "endpoint": "ne", + "payload": { + "tagger": "ner-fast-with-lowercase", + "text": "REL is a modular Entity Linking package that can both be integrated in existing pipelines or be used as an API.", + "spans": [], + }, + }, + { + "endpoint": "ne", + "payload": { + "tagger": "ner-fast", + "text": "If you're going to try, go all the way - Charles Bukowski.", + "spans": [(41, 16)], }, - { - "speaker": "SYSTEM", - "utterance": "Some people are allergic to histamine in tomatoes.", + }, + { + "endpoint": "conv", + "payload": { + "tagger": "default", + "text": [ + { + "speaker": "USER", + "utterance": "I am allergic to tomatoes but we have a lot of famous Italian restaurants here in London.", + }, + { + "speaker": "SYSTEM", + "utterance": "Some people are allergic to histamine in tomatoes.", + }, + { + "speaker": "USER", + "utterance": "Talking of food, can you recommend me a restaurant in my city for our anniversary?", + }, + ], }, - { - "speaker": - "USER", - "utterance": - "Talking of food, can you recommend me a restaurant in my city for our anniversary?", + }, + { + "endpoint": "ne_concept", + "payload": {}, + }, + { + "endpoint": "this-endpoint-does-not-exist", + "payload": {}, + }, + { + "endpoint": "", + "payload": { + "text": "Hello world.", + "this-argument-does-not-exist": None, }, - ] -} + }, +) +for item in items: + endpoint = item["endpoint"] + payload = item["payload"] -for endpoint, myjson in ( - ('', text1), - ('conversation/', conv1) - ): - print('Input API:') - print(myjson) + print("Request body:") + print(payload) + print() + print("Response:") + print(requests.post(f"http://{host}:{port}/{endpoint}", json=payload).json()) + print() + print("----------------------------") print() - print('Output API:') - print(requests.post(f"http://{host}:{port}/{endpoint}", json=myjson).json()) - print('----------------------------') - diff --git a/src/REL/crel/conv_el.py b/src/REL/crel/conv_el.py index 868f962..d67f7e1 100644 --- a/src/REL/crel/conv_el.py +++ b/src/REL/crel/conv_el.py @@ -5,25 +5,31 @@ from .bert_md import BERT_MD from .s2e_pe import pe_data from .s2e_pe.pe import EEMD, PEMD -from REL.response_model import ResponseModel +from REL.response_handler import ResponseHandler class ConvEL: def __init__( - self, base_url=".", wiki_version="wiki_2019", ed_model=None, user_config=None, threshold=0 - ): + self, + base_url=".", + wiki_version="wiki_2019", + ed_model=None, + user_config=None, + threshold=0, + ner_model="bert_conv-td", + ): self.threshold = threshold self.wiki_version = wiki_version self.base_url = base_url - self.file_pretrained = str(Path(base_url) / "bert_conv-td") + self.file_pretrained = str(Path(base_url) / ner_model) self.bert_md = BERT_MD(self.file_pretrained) if not ed_model: ed_model = self._default_ed_model() - self.response_model = ResponseModel(self.base_url, self.wiki_version, model=ed_model) + self.response_handler = ResponseHandler(self.base_url, self.wiki_version, model=ed_model) self.eemd = EEMD(s2e_pe_model=str(Path(base_url) / "s2e_ast_onto")) self.pemd = PEMD() @@ -155,7 +161,7 @@ def annotate(self, conv): def ed(self, text, spans): """Change tuple to list to match the output format of REL API.""" - response = self.response_model.generate_response(text=text, spans=spans) + response = self.response_handler.generate_response(text=text, spans=spans) return [list(ent) for ent in response] diff --git a/src/REL/response_model.py b/src/REL/response_handler.py similarity index 78% rename from src/REL/response_model.py rename to src/REL/response_handler.py index b41da49..0193607 100644 --- a/src/REL/response_model.py +++ b/src/REL/response_handler.py @@ -4,8 +4,20 @@ from REL.mention_detection import MentionDetection from REL.utils import process_results +MD_MODELS = {} -class ResponseModel: +def _get_mention_detection_model(base_url, wiki_version): + """Return instance of previously generated model for the same wiki version.""" + try: + md_model = MD_MODELS[(base_url, wiki_version)] + except KeyError: + md_model = MentionDetection(base_url, wiki_version) + MD_MODELS[base_url, wiki_version] = md_model + + return md_model + + +class ResponseHandler: API_DOC = "API_DOC" def __init__(self, base_url, wiki_version, model, tagger_ner=None): @@ -16,7 +28,7 @@ def __init__(self, base_url, wiki_version, model, tagger_ner=None): self.wiki_version = wiki_version self.custom_ner = not isinstance(tagger_ner, SequenceTagger) - self.mention_detection = MentionDetection(base_url, wiki_version) + self.mention_detection = _get_mention_detection_model(base_url, wiki_version) def generate_response(self, *, diff --git a/src/REL/server.py b/src/REL/server.py index b321ad2..7942dc3 100644 --- a/src/REL/server.py +++ b/src/REL/server.py @@ -1,49 +1,252 @@ -from REL.response_model import ResponseModel +from REL.response_handler import ResponseHandler from fastapi import FastAPI -from pydantic import BaseModel, Field -from typing import List, Optional, Literal +from fastapi.responses import JSONResponse +from pydantic import BaseModel +from pydantic import Field +from typing import List, Optional, Literal, Union, Annotated, Tuple + +DEBUG = False app = FastAPI() -@app.get("/") -def root(): - """Returns server status.""" - return { - "schemaVersion": 1, - "label": "status", - "message": "up", - "color": "green", - } +Span = Tuple[int, int] + + +class NamedEntityConfig(BaseModel): + """Config for named entity linking. For more information, see + + """ -class EntityConfig(BaseModel): text: str = Field(..., description="Text for entity linking or disambiguation.") - spans: List[str] = Field(..., description="Spans for entity disambiguation.") + spans: Optional[List[Span]] = Field( + None, + description=( + """ +For EL: the spans field needs to be set to an empty list. + +For ED: spans should consist of a list of tuples, where each tuple refers to +the start position and length of a mention. + +This is used when mentions are already identified and disambiguation is only +needed. Each tuple represents start position and length of mention (in +characters); e.g., `[(0, 8), (15,11)]` for mentions 'Nijmegen' and +'Netherlands' in text 'Nijmegen is in the Netherlands'. +""" + ), + ) + tagger: Literal[ + "ner-fast", + "ner-fast-with-lowercase", + ] = Field("ner-fast", description="NER tagger to use.") + + class Config: + schema_extra = { + "example": { + "text": "If you're going to try, go all the way - Charles Bukowski.", + "spans": [(41, 16)], + "tagger": "ner-fast", + } + } + + def response(self): + """Return response for request.""" + handler = handlers[self.tagger] + response = handler.generate_response(text=self.text, spans=self.spans) + return response -@app.post("/") -def root(config: EntityConfig): - """Submit your text here for entity disambiguation or linking.""" - response = handler.generate_response(text=config.text, spans=config.spans) - return response +class NamedEntityConceptConfig(BaseModel): + """Config for named entity linking. Not yet implemented.""" + + def response(self): + """Return response for request.""" + response = JSONResponse( + content={"msg": "Mode `ne_concept` has not been implemeted."}, + status_code=501, + ) + return response class ConversationTurn(BaseModel): - speaker: Literal["USER", "SYSTEM"] = Field(..., description="Speaker for this turn.") - utterance: str = Field(..., description="Input utterance.") + """Specify turns in a conversation. Each turn has a `speaker` + and an `utterance`.""" + + speaker: Literal["USER", "SYSTEM"] = Field( + ..., description="Speaker for this turn, must be one of `USER` or `SYSTEM`." + ) + utterance: str = Field(..., description="Input utterance to be annotated.") + + class Config: + schema_extra = { + "example": { + "speaker": "USER", + "utterance": "I am allergic to tomatoes but we have a lot of famous Italian restaurants here in London.", + } + } class ConversationConfig(BaseModel): - text: List[ConversationTurn] = Field(..., description="Conversation as list of turns between two speakers.") + """Config for conversational entity linking. For more information: + . + """ + + text: List[ConversationTurn] = Field( + ..., description="Conversation as list of turns between two speakers." + ) + tagger: Literal[ + "default", + ] = Field("default", description="NER tagger to use.") + + class Config: + schema_extra = { + "example": { + "text": ( + { + "speaker": "USER", + "utterance": "I am allergic to tomatoes but we have a lot of famous Italian restaurants here in London.", + }, + { + "speaker": "SYSTEM", + "utterance": "Some people are allergic to histamine in tomatoes.", + }, + { + "speaker": "USER", + "utterance": "Talking of food, can you recommend me a restaurant in my city for our anniversary?", + }, + ), + "tagger": "default", + } + } + + def response(self): + """Return response for request.""" + text = self.dict()["text"] + conv_handler = conv_handlers[self.tagger] + response = conv_handler.annotate(text) + return response + + +class TurnAnnotation(BaseModel): + __root__: List[Union[int, str]] = Field( + ..., + min_items=4, + max_items=4, + description=""" +The 4 values of the annotation represent the start index of the word, +length of the word, the annotated word, and the prediction. +""", + ) + + class Config: + schema_extra = {"example": [82, 6, "London", "London"]} + + +class SystemResponse(ConversationTurn): + """Return input when the speaker equals 'SYSTEM'.""" + + speaker: str = "SYSTEM" + class Config: + schema_extra = { + "example": { + "speaker": "SYSTEM", + "utterance": "Some people are allergic to histamine in tomatoes.", + }, + } -@app.post("/conversation/") -def conversation(config: ConversationConfig): + +class UserResponse(ConversationTurn): + """Return annotations when the speaker equals 'USER'.""" + + speaker: str = "USER" + annotations: List[TurnAnnotation] = Field(..., description="List of annotations.") + + class Config: + schema_extra = { + "example": { + "speaker": "USER", + "utterance": "I am allergic to tomatoes but we have a lot of famous Italian restaurants here in London.", + "annotations": [ + [17, 8, "tomatoes", "Tomato"], + [54, 19, "Italian restaurants", "Italian_cuisine"], + [82, 6, "London", "London"], + ], + }, + } + + +TurnResponse = Union[UserResponse, SystemResponse] + + +class NEAnnotation(BaseModel): + """Annotation for named entity linking.""" + + __root__: List[Union[int, str, float]] = Field( + ..., + min_items=7, + max_items=7, + description=""" +The 7 values of the annotation represent the +start index, end index, the annotated word, prediction, ED confidence, MD confidence, and tag. +""", + ) + + class Config: + schema_extra = { + "example": [41, 16, "Charles Bukowski", "Charles_Bukowski", 0, 0, "NULL"] + } + + +class StatusResponse(BaseModel): + schemaVersion: int + label: str + message: str + color: str + + +@app.get("/", response_model=StatusResponse) +def server_status(): + """Returns server status.""" + return { + "schemaVersion": 1, + "label": "status", + "message": "up", + "color": "green", + } + + +@app.post("/", response_model=List[NEAnnotation]) +@app.post("/ne", response_model=List[NEAnnotation]) +def named_entity_linking(config: NamedEntityConfig): + """Submit your text here for entity disambiguation or linking. + + The REL annotation mode can be selected by changing the path. + use `/` or `/ne/` for annotating regular text with named + entities (default), `/ne_concept/` for regular text with both concepts and + named entities, and `/conv/` for conversations with both concepts and + named entities. + """ + if DEBUG: + return [] + return config.response() + + +@app.post("/conv", response_model=List[TurnResponse]) +def conversational_entity_linking(config: ConversationConfig): """Submit your text here for conversational entity linking.""" - text = config.dict()['text'] - response = conv_handler.annotate(text) - return response + if DEBUG: + return [] + return config.response() + + +@app.post("/ne_concept", response_model=List[NEAnnotation]) +def conceptual_named_entity_linking(config: NamedEntityConceptConfig): + """Submit your text here for conceptual entity disambiguation or linking.""" + if DEBUG: + return [] + return config.response() if __name__ == "__main__": @@ -54,22 +257,34 @@ def conversation(config: ConversationConfig): p.add_argument("base_url") p.add_argument("wiki_version") p.add_argument("--ed-model", default="ed-wiki-2019") - p.add_argument("--ner-model", default="ner-fast") + p.add_argument("--ner-model", default="ner-fast", nargs="+") p.add_argument("--bind", "-b", metavar="ADDRESS", default="0.0.0.0") p.add_argument("--port", "-p", default=5555, type=int) args = p.parse_args() - from REL.crel.conv_el import ConvEL - from REL.entity_disambiguation import EntityDisambiguation - from REL.ner import load_flair_ner + if not DEBUG: + from REL.crel.conv_el import ConvEL + from REL.entity_disambiguation import EntityDisambiguation + from REL.ner import load_flair_ner - ner_model = load_flair_ner(args.ner_model) - ed_model = EntityDisambiguation( - args.base_url, args.wiki_version, {"mode": "eval", "model_path": args.ed_model} - ) + ed_model = EntityDisambiguation( + args.base_url, + args.wiki_version, + {"mode": "eval", "model_path": args.ed_model}, + ) + + handlers = {} - handler = ResponseModel(args.base_url, args.wiki_version, ed_model, ner_model) + for ner_model_name in args.ner_model: + print("Loading NER model:", ner_model_name) + ner_model = load_flair_ner(ner_model_name) + handler = ResponseHandler( + args.base_url, args.wiki_version, ed_model, ner_model + ) + handlers[ner_model_name] = handler - conv_handler = ConvEL(args.base_url, args.wiki_version, ed_model=ed_model) + conv_handlers = { + "default": ConvEL(args.base_url, args.wiki_version, ed_model=ed_model) + } uvicorn.run(app, port=args.port, host=args.bind)