From 39a31a5cc2c85a99df604ab0e9be10746d3a42e2 Mon Sep 17 00:00:00 2001 From: Stef Smeets Date: Mon, 23 Jan 2023 15:25:15 +0100 Subject: [PATCH 01/14] Support multiple models in server api --- scripts/test_server.py | 79 ++++++++++++++++++++++++--------------- src/REL/crel/conv_el.py | 12 ++++-- src/REL/response_model.py | 14 ++++++- src/REL/server.py | 30 ++++++++++++--- 4 files changed, 94 insertions(+), 41 deletions(-) diff --git a/scripts/test_server.py b/scripts/test_server.py index 2953248..2f09955 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_models 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_models ner-fast ner-fast-with-lowercase # # Set $REL_BASE_URL to where your data are stored (`base_url`) # @@ -22,41 +22,58 @@ 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.", - }, +inputs = ( + { + 'endpoint': '', + 'payload': { - "speaker": "SYSTEM", - "utterance": "Some people are allergic to histamine in tomatoes.", - }, + 'model': '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': '', + 'payload': { + 'model': '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': 'conversation', + 'payload': { - "speaker": - "USER", - "utterance": - "Talking of food, can you recommend me a restaurant in my city for our anniversary?", - }, - ] -} + 'model': '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?', + }, + ], + } + }, +) +for inp in inputs: + endpoint = inp['endpoint'] + payload = inp['payload'] -for endpoint, myjson in ( - ('', text1), - ('conversation/', conv1) - ): print('Input API:') - print(myjson) + print(payload) print() print('Output API:') - print(requests.post(f"http://{host}:{port}/{endpoint}", json=myjson).json()) + print(requests.post(f'http://{host}:{port}/{endpoint}', json=payload).json()) + print() print('----------------------------') + print() diff --git a/src/REL/crel/conv_el.py b/src/REL/crel/conv_el.py index 868f962..9580cc3 100644 --- a/src/REL/crel/conv_el.py +++ b/src/REL/crel/conv_el.py @@ -10,13 +10,19 @@ 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) diff --git a/src/REL/response_model.py b/src/REL/response_model.py index b41da49..a71e938 100644 --- a/src/REL/response_model.py +++ b/src/REL/response_model.py @@ -4,6 +4,18 @@ from REL.mention_detection import MentionDetection from REL.utils import process_results +MD_MODELS = {} + +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 ResponseModel: API_DOC = "API_DOC" @@ -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..8db1b1e 100644 --- a/src/REL/server.py +++ b/src/REL/server.py @@ -19,12 +19,19 @@ def root(): class EntityConfig(BaseModel): text: str = Field(..., description="Text for entity linking or disambiguation.") - spans: List[str] = Field(..., description="Spans for entity disambiguation.") - + spans: List[str] = Field(..., 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.")) + model: Literal[ + "ner-fast", + "ner-fast-with-lowercase", + ] = Field("ner-fast", description="NER model to use.") @app.post("/") def root(config: EntityConfig): """Submit your text here for entity disambiguation or linking.""" + handler = handlers[config.model] response = handler.generate_response(text=config.text, spans=config.spans) return response @@ -36,13 +43,19 @@ class ConversationTurn(BaseModel): class ConversationConfig(BaseModel): text: List[ConversationTurn] = Field(..., description="Conversation as list of turns between two speakers.") + model: Literal[ + "default", + ] = Field("default", description="NER model to use.") @app.post("/conversation/") def conversation(config: ConversationConfig): """Submit your text here for conversational entity linking.""" text = config.dict()['text'] + + conv_handler = conv_handlers[config.model] response = conv_handler.annotate(text) + return response @@ -54,7 +67,7 @@ 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() @@ -63,13 +76,18 @@ def conversation(config: ConversationConfig): 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} ) - handler = ResponseModel(args.base_url, args.wiki_version, ed_model, ner_model) + handlers = {} + + for ner_model_name in args.ner_model: + print('Loading NER model:', ner_model_name) + ner_model = load_flair_ner(ner_model_name) + handler = ResponseModel(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) From d715082b8f55ba52bbb08be22e27aafcc330a162 Mon Sep 17 00:00:00 2001 From: Stef Smeets Date: Mon, 30 Jan 2023 11:28:43 +0100 Subject: [PATCH 02/14] Get flat api to work with multiple request bodies + default type --- scripts/test_server.py | 64 +++++++++++++++------------------- src/REL/server.py | 78 +++++++++++++++++++++++++++--------------- 2 files changed, 78 insertions(+), 64 deletions(-) diff --git a/scripts/test_server.py b/scripts/test_server.py index 2f09955..79b0585 100644 --- a/scripts/test_server.py +++ b/scripts/test_server.py @@ -24,49 +24,39 @@ inputs = ( { - 'endpoint': '', - 'payload': - { - 'model': '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': [], - } + 'model': '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': '', - 'payload': { - 'model': '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': [], - } + 'mode': 'ne', + 'model': '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': 'conversation', - 'payload': - { - 'model': '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?', - }, - ], - } - }, + 'mode': 'conv', + 'model': '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?', + }, + ], + } ) -for inp in inputs: - endpoint = inp['endpoint'] - payload = inp['payload'] +for payload in inputs: + endpoint = '' print('Input API:') print(payload) diff --git a/src/REL/server.py b/src/REL/server.py index 8db1b1e..7851493 100644 --- a/src/REL/server.py +++ b/src/REL/server.py @@ -2,7 +2,7 @@ from fastapi import FastAPI from pydantic import BaseModel, Field -from typing import List, Optional, Literal +from typing import List, Optional, Literal, Union, Annotated app = FastAPI() @@ -16,8 +16,9 @@ def root(): "color": "green", } +class NamedEntityConfig(BaseModel): + mode: Literal['ne'] -class EntityConfig(BaseModel): text: str = Field(..., description="Text for entity linking or disambiguation.") spans: List[str] = Field(..., description=( "For EL: the spans field needs to be set to an empty list. " @@ -28,12 +29,9 @@ class EntityConfig(BaseModel): "ner-fast-with-lowercase", ] = Field("ner-fast", description="NER model to use.") -@app.post("/") -def root(config: EntityConfig): - """Submit your text here for entity disambiguation or linking.""" - handler = handlers[config.model] - response = handler.generate_response(text=config.text, spans=config.spans) - return response + +class NamedEntityConceptConfig(BaseModel): + mode: Literal['ne_concept'] class ConversationTurn(BaseModel): @@ -42,19 +40,45 @@ class ConversationTurn(BaseModel): class ConversationConfig(BaseModel): + mode: Literal['conv'] + text: List[ConversationTurn] = Field(..., description="Conversation as list of turns between two speakers.") model: Literal[ "default", ] = Field("default", description="NER model to use.") -@app.post("/conversation/") -def conversation(config: ConversationConfig): - """Submit your text here for conversational entity linking.""" - text = config.dict()['text'] +class DefaultConfig(NamedEntityConfig): + mode: str = 'ne' + + +Config = Annotated[Union[DefaultConfig, + NamedEntityConfig, + NamedEntityConceptConfig, + ConversationConfig], + Field(discriminator='mode')] + + +@app.post("/") +def root(config: Config): + """Submit your text here for entity disambiguation or linking.""" + + print(config) + + return config + + if config.mode == 'conv': + text = config.dict()['text'] + + conv_handler = conv_handlers[config.model] + response = conv_handler.annotate(text) + + elif config.mode == 'ne': + handler = handlers[config.model] + response = handler.generate_response(text=config.text, spans=config.spans) - conv_handler = conv_handlers[config.model] - response = conv_handler.annotate(text) + elif config.mode == 'ne_concept': + raise NotImplementedError return response @@ -72,22 +96,22 @@ def conversation(config: ConversationConfig): 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 + # from REL.crel.conv_el import ConvEL + # from REL.entity_disambiguation import EntityDisambiguation + # from REL.ner import load_flair_ner - 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 = {} + # handlers = {} - for ner_model_name in args.ner_model: - print('Loading NER model:', ner_model_name) - ner_model = load_flair_ner(ner_model_name) - handler = ResponseModel(args.base_url, args.wiki_version, ed_model, ner_model) - handlers[ner_model_name] = handler + # for ner_model_name in args.ner_model: + # print('Loading NER model:', ner_model_name) + # ner_model = load_flair_ner(ner_model_name) + # handler = ResponseModel(args.base_url, args.wiki_version, ed_model, ner_model) + # handlers[ner_model_name] = handler - conv_handlers = {'default': 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) From 58972838796e0bfce4f46ecd700a71cac70b3940 Mon Sep 17 00:00:00 2001 From: Stef Smeets Date: Mon, 30 Jan 2023 11:44:30 +0100 Subject: [PATCH 03/14] Add responses as methods and fix discriminator --- scripts/test_server.py | 11 ++++++- src/REL/server.py | 70 ++++++++++++++++++++++-------------------- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/scripts/test_server.py b/scripts/test_server.py index 79b0585..e1612e3 100644 --- a/scripts/test_server.py +++ b/scripts/test_server.py @@ -52,7 +52,16 @@ 'utterance': 'Talking of food, can you recommend me a restaurant in my city for our anniversary?', }, ], - } + }, + { + 'mode': 'ne_concept', + 'model': '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': [], + }, + { + 'mode': 'fail', + }, ) for payload in inputs: diff --git a/src/REL/server.py b/src/REL/server.py index 7851493..073efa1 100644 --- a/src/REL/server.py +++ b/src/REL/server.py @@ -29,10 +29,20 @@ class NamedEntityConfig(BaseModel): "ner-fast-with-lowercase", ] = Field("ner-fast", description="NER model to use.") + def response(self): + """Return response for request.""" + handler = handlers[self.model] + response = handler.generate_response(text=self.text, spans=self.spans) + return response + class NamedEntityConceptConfig(BaseModel): mode: Literal['ne_concept'] + def response(self): + """Return response for request.""" + return "Not implemented" + class ConversationTurn(BaseModel): speaker: Literal["USER", "SYSTEM"] = Field(..., description="Speaker for this turn.") @@ -47,15 +57,22 @@ class ConversationConfig(BaseModel): "default", ] = Field("default", description="NER model to use.") + def response(self): + """Return response for request.""" + text = self.dict()['text'] + conv_handler = conv_handlers[self.model] + response = conv_handler.annotate(text) + return response + class DefaultConfig(NamedEntityConfig): - mode: str = 'ne' + mode: Literal['ne'] = 'ne' -Config = Annotated[Union[DefaultConfig, - NamedEntityConfig, +Config = Annotated[Union[NamedEntityConfig, NamedEntityConceptConfig, - ConversationConfig], + ConversationConfig, + DefaultConfig], # default must be last Field(discriminator='mode')] @@ -63,24 +80,11 @@ class DefaultConfig(NamedEntityConfig): def root(config: Config): """Submit your text here for entity disambiguation or linking.""" - print(config) - - return config - - if config.mode == 'conv': - text = config.dict()['text'] - - conv_handler = conv_handlers[config.model] - response = conv_handler.annotate(text) - - elif config.mode == 'ne': - handler = handlers[config.model] - response = handler.generate_response(text=config.text, spans=config.spans) + # print(f'\nmode: {config.mode} -> {type(config)}\n') - elif config.mode == 'ne_concept': - raise NotImplementedError + # return config - return response + return config.response() if __name__ == "__main__": @@ -96,22 +100,22 @@ def root(config: Config): 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 + from REL.crel.conv_el import ConvEL + from REL.entity_disambiguation import EntityDisambiguation + from REL.ner import load_flair_ner - # 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 = {} + handlers = {} - # for ner_model_name in args.ner_model: - # print('Loading NER model:', ner_model_name) - # ner_model = load_flair_ner(ner_model_name) - # handler = ResponseModel(args.base_url, args.wiki_version, ed_model, ner_model) - # handlers[ner_model_name] = handler + for ner_model_name in args.ner_model: + print('Loading NER model:', ner_model_name) + ner_model = load_flair_ner(ner_model_name) + handler = ResponseModel(args.base_url, args.wiki_version, ed_model, ner_model) + handlers[ner_model_name] = handler - # conv_handlers = {'default': 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) From 5a37f0adc010c44cb020b818b36d1ff7109411af Mon Sep 17 00:00:00 2001 From: Stef Smeets Date: Mon, 30 Jan 2023 13:39:21 +0100 Subject: [PATCH 04/14] Return 501 Not Implemented for ne_concept --- src/REL/server.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/REL/server.py b/src/REL/server.py index 073efa1..ee95550 100644 --- a/src/REL/server.py +++ b/src/REL/server.py @@ -1,7 +1,9 @@ from REL.response_model import ResponseModel from fastapi import FastAPI -from pydantic import BaseModel, Field +from fastapi.responses import JSONResponse +from pydantic import BaseModel +from pydantic import Field from typing import List, Optional, Literal, Union, Annotated app = FastAPI() @@ -41,8 +43,11 @@ class NamedEntityConceptConfig(BaseModel): def response(self): """Return response for request.""" - return "Not implemented" - + 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.") From 8074289969eafbaf118f17ea52ae731135467552 Mon Sep 17 00:00:00 2001 From: Stef Smeets Date: Mon, 30 Jan 2023 13:41:47 +0100 Subject: [PATCH 05/14] Rename model -> tagger --- scripts/test_server.py | 13 ++--- src/REL/server.py | 111 +++++++++++++++++++++++++---------------- 2 files changed, 74 insertions(+), 50 deletions(-) diff --git a/scripts/test_server.py b/scripts/test_server.py index e1612e3..bd2a005 100644 --- a/scripts/test_server.py +++ b/scripts/test_server.py @@ -24,19 +24,19 @@ inputs = ( { - 'model': 'ner-fast', + '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': [], }, { 'mode': 'ne', - 'model': 'ner-fast-with-lowercase', + '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': [], }, { 'mode': 'conv', - 'model': 'default', + 'tagger': 'default', 'text': [ { @@ -55,13 +55,14 @@ }, { 'mode': 'ne_concept', - 'model': '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': [], }, { 'mode': 'fail', }, + { + 'text': 'Hello world.', + 'this-argument-does-not-exist': None, + }, ) for payload in inputs: diff --git a/src/REL/server.py b/src/REL/server.py index ee95550..b7baccf 100644 --- a/src/REL/server.py +++ b/src/REL/server.py @@ -6,8 +6,11 @@ from pydantic import Field from typing import List, Optional, Literal, Union, Annotated +DEBUG = False + app = FastAPI() + @app.get("/") def root(): """Returns server status.""" @@ -18,78 +21,91 @@ def root(): "color": "green", } + class NamedEntityConfig(BaseModel): - mode: Literal['ne'] + mode: Literal["ne"] text: str = Field(..., description="Text for entity linking or disambiguation.") - spans: List[str] = Field(..., 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.")) - model: Literal[ - "ner-fast", + spans: List[str] = Field( + ..., + 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." + ), + ) + tagger: Literal[ + "ner-fast", "ner-fast-with-lowercase", - ] = Field("ner-fast", description="NER model to use.") + ] = Field("ner-fast", description="NER tagger to use.") def response(self): """Return response for request.""" - handler = handlers[self.model] + handler = handlers[self.tagger] response = handler.generate_response(text=self.text, spans=self.spans) return response class NamedEntityConceptConfig(BaseModel): - mode: Literal['ne_concept'] + mode: Literal["ne_concept"] def response(self): """Return response for request.""" response = JSONResponse( - content = {'msg': 'Mode `ne_concept` has not been implemeted.'}, + 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.") + speaker: Literal["USER", "SYSTEM"] = Field( + ..., description="Speaker for this turn." + ) utterance: str = Field(..., description="Input utterance.") class ConversationConfig(BaseModel): - mode: Literal['conv'] + mode: Literal["conv"] - text: List[ConversationTurn] = Field(..., description="Conversation as list of turns between two speakers.") - model: Literal[ - "default", - ] = Field("default", description="NER model to use.") + text: List[ConversationTurn] = Field( + ..., description="Conversation as list of turns between two speakers." + ) + tagger: Literal[ + "default", + ] = Field("default", description="NER tagger to use.") def response(self): """Return response for request.""" - text = self.dict()['text'] - conv_handler = conv_handlers[self.model] + text = self.dict()["text"] + conv_handler = conv_handlers[self.tagger] response = conv_handler.annotate(text) return response class DefaultConfig(NamedEntityConfig): - mode: Literal['ne'] = 'ne' + mode: Literal["ne"] = "ne" -Config = Annotated[Union[NamedEntityConfig, - NamedEntityConceptConfig, - ConversationConfig, - DefaultConfig], # default must be last - Field(discriminator='mode')] +Config = Annotated[ + Union[ + NamedEntityConfig, + NamedEntityConceptConfig, + ConversationConfig, + DefaultConfig, # default must be last + ], + Field(discriminator="mode"), +] @app.post("/") def root(config: Config): """Submit your text here for entity disambiguation or linking.""" - - # print(f'\nmode: {config.mode} -> {type(config)}\n') - - # return config - - return config.response() + if DEBUG: + print(f"\nmode: {config.mode} -> {type(config)}\n") + return config + else: + return config.response() if __name__ == "__main__": @@ -105,22 +121,29 @@ def root(config: Config): 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 - 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 = {} + handlers = {} - for ner_model_name in args.ner_model: - print('Loading NER model:', ner_model_name) - ner_model = load_flair_ner(ner_model_name) - handler = ResponseModel(args.base_url, args.wiki_version, ed_model, ner_model) - handlers[ner_model_name] = handler + for ner_model_name in args.ner_model: + print("Loading NER model:", ner_model_name) + ner_model = load_flair_ner(ner_model_name) + handler = ResponseModel( + args.base_url, args.wiki_version, ed_model, ner_model + ) + handlers[ner_model_name] = handler - conv_handlers = {'default': 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) From b589259ba860817a73779c5ceaa6d07ca0f36b71 Mon Sep 17 00:00:00 2001 From: Stef Smeets Date: Mon, 30 Jan 2023 15:28:48 +0100 Subject: [PATCH 06/14] Add documentation --- docs/tutorials/server.md | 86 +++++++++++++++++++++++ docs/tutorials/systemd_instructions.md | 46 ------------ mkdocs.yml | 2 +- scripts/code_tutorials/run_server.py | 34 --------- scripts/code_tutorials/run_server_temp.py | 72 ------------------- 5 files changed, 87 insertions(+), 153 deletions(-) create mode 100644 docs/tutorials/server.md delete mode 100644 docs/tutorials/systemd_instructions.md delete mode 100644 scripts/code_tutorials/run_server.py delete mode 100644 scripts/code_tutorials/run_server_temp.py 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..f050fa6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,7 +8,7 @@ nav: - 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) From 7050b4cc2b72e747df3b44c3f1fd16c6f299d211 Mon Sep 17 00:00:00 2001 From: Stef Smeets Date: Mon, 30 Jan 2023 17:12:50 +0100 Subject: [PATCH 07/14] Add test for spans and ResponseModel->ResponseHandler --- scripts/test_server.py | 6 ++++++ src/REL/crel/conv_el.py | 6 +++--- src/REL/{response_model.py => response_handler.py} | 2 +- src/REL/server.py | 4 ++-- 4 files changed, 12 insertions(+), 6 deletions(-) rename src/REL/{response_model.py => response_handler.py} (98%) diff --git a/scripts/test_server.py b/scripts/test_server.py index bd2a005..4d58625 100644 --- a/scripts/test_server.py +++ b/scripts/test_server.py @@ -34,6 +34,12 @@ 'text': 'REL is a modular Entity Linking package that can both be integrated in existing pipelines or be used as an API.', 'spans': [], }, + { + 'mode': 'ne', + 'tagger': 'ner-fast', + 'text': "If you're going to try, go all the way - Charles Bukowski.", + 'spans': [(41, 16)], + }, { 'mode': 'conv', 'tagger': 'default', diff --git a/src/REL/crel/conv_el.py b/src/REL/crel/conv_el.py index 9580cc3..d67f7e1 100644 --- a/src/REL/crel/conv_el.py +++ b/src/REL/crel/conv_el.py @@ -5,7 +5,7 @@ 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: @@ -29,7 +29,7 @@ def __init__( 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() @@ -161,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 98% rename from src/REL/response_model.py rename to src/REL/response_handler.py index a71e938..0193607 100644 --- a/src/REL/response_model.py +++ b/src/REL/response_handler.py @@ -17,7 +17,7 @@ def _get_mention_detection_model(base_url, wiki_version): return md_model -class ResponseModel: +class ResponseHandler: API_DOC = "API_DOC" def __init__(self, base_url, wiki_version, model, tagger_ner=None): diff --git a/src/REL/server.py b/src/REL/server.py index b7baccf..4fc28e7 100644 --- a/src/REL/server.py +++ b/src/REL/server.py @@ -1,4 +1,4 @@ -from REL.response_model import ResponseModel +from REL.response_handler import ResponseHandler from fastapi import FastAPI from fastapi.responses import JSONResponse @@ -137,7 +137,7 @@ def root(config: Config): for ner_model_name in args.ner_model: print("Loading NER model:", ner_model_name) ner_model = load_flair_ner(ner_model_name) - handler = ResponseModel( + handler = ResponseHandler( args.base_url, args.wiki_version, ed_model, ner_model ) handlers[ner_model_name] = handler From 8b50696d5a5c567edae86a62f7f0172739077cc1 Mon Sep 17 00:00:00 2001 From: Stef Smeets Date: Mon, 6 Feb 2023 10:58:23 +0100 Subject: [PATCH 08/14] Implement endpoints with response models for different linking modes --- scripts/test_server.py | 101 +++++++++++++++------------- src/REL/server.py | 146 ++++++++++++++++++++++++++++++----------- 2 files changed, 163 insertions(+), 84 deletions(-) diff --git a/scripts/test_server.py b/scripts/test_server.py index 4d58625..be9c330 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 --ner_models ner-fast ner-fast-with-lowercase +# 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 --ner_models ner-fast ner-fast-with-lowercase +# 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,67 +19,80 @@ # -host = 'localhost' -port = '5555' +host = "localhost" +port = "5555" -inputs = ( +items = ( { - '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": "", + "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": [], + }, }, { - 'mode': 'ne', - '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-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": [], + }, }, { - 'mode': 'ne', - 'tagger': 'ner-fast', - 'text': "If you're going to try, go all the way - Charles Bukowski.", - 'spans': [(41, 16)], + "endpoint": "ne", + "payload": { + "tagger": "ner-fast", + "text": "If you're going to try, go all the way - Charles Bukowski.", + "spans": [(41, 16)], + }, }, { - 'mode': 'conv', - '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?', - }, - ], + "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?", + }, + ], + }, }, { - 'mode': 'ne_concept', + "endpoint": "ne_concept", + "payload": {}, }, { - 'mode': 'fail', + "endpoint": "this-endpoint-does-not-exist", + "payload": {}, }, { - 'text': 'Hello world.', - 'this-argument-does-not-exist': None, + "endpoint": "", + "payload": { + "text": "Hello world.", + "this-argument-does-not-exist": None, + }, }, ) -for payload in inputs: - endpoint = '' +for item in items: + endpoint = item['endpoint'] + payload = item['payload'] - print('Input API:') + print("Request body:") print(payload) print() - print('Output API:') - print(requests.post(f'http://{host}:{port}/{endpoint}', json=payload).json()) + print("Response:") + print(requests.post(f"http://{host}:{port}/{endpoint}", json=payload).json()) print() - print('----------------------------') + print("----------------------------") print() - diff --git a/src/REL/server.py b/src/REL/server.py index 4fc28e7..9d86eaf 100644 --- a/src/REL/server.py +++ b/src/REL/server.py @@ -4,34 +4,34 @@ from fastapi.responses import JSONResponse from pydantic import BaseModel from pydantic import Field -from typing import List, Optional, Literal, Union, Annotated +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): - mode: Literal["ne"] - + """Config for named entity linking. For more information, see + + """ text: str = Field(..., description="Text for entity linking or disambiguation.") - spans: List[str] = Field( - ..., + 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." + """ +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[ @@ -47,7 +47,7 @@ def response(self): class NamedEntityConceptConfig(BaseModel): - mode: Literal["ne_concept"] + """Config for named entity linking. Not yet implemented.""" def response(self): """Return response for request.""" @@ -59,15 +59,19 @@ def response(self): class ConversationTurn(BaseModel): + """Specify turns in a conversation. Each turn has a `speaker` + and an `utterance`.""" + speaker: Literal["USER", "SYSTEM"] = Field( - ..., description="Speaker for this turn." + ..., description="Speaker for this turn, must be one of `USER` or `SYSTEM`." ) - utterance: str = Field(..., description="Input utterance.") + utterance: str = Field(..., description="Input utterance to be annotated.") class ConversationConfig(BaseModel): - mode: Literal["conv"] - + """Config for conversational entity linking. For more information: + . + """ text: List[ConversationTurn] = Field( ..., description="Conversation as list of turns between two speakers." ) @@ -83,29 +87,91 @@ def response(self): return response -class DefaultConfig(NamedEntityConfig): - mode: Literal["ne"] = "ne" +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 SystemResponse(ConversationTurn): + """Return input when the speaker equals 'SYSTEM'.""" + speaker: str = 'SYSTEM' + +class UserResponse(ConversationTurn): + """Return annotations when the speaker equals 'USER'.""" + speaker: str = 'USER' + annotations: List[TurnAnnotation] = Field( + ..., + description="List of annotations.") + +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. +""") -Config = Annotated[ - Union[ - NamedEntityConfig, - NamedEntityConceptConfig, - ConversationConfig, - DefaultConfig, # default must be last - ], - Field(discriminator="mode"), -] +@app.get("/") +def root(): + """Returns server status.""" + return { + "schemaVersion": 1, + "label": "status", + "message": "up", + "color": "green", + } -@app.post("/") -def root(config: Config): +@app.post("/", response_model=List[NEAnnotation]) +def root(config: NamedEntityConfig): + """Submit your text here for entity disambiguation or linking. + + The REL annotation mode can be selected by changing the endpoint. + 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("/ne", response_model=List[NEAnnotation]) +def root(config: NamedEntityConfig): """Submit your text here for entity disambiguation or linking.""" if DEBUG: - print(f"\nmode: {config.mode} -> {type(config)}\n") - return config - else: - return config.response() + return [] + return config.response() + + +@app.post("/conv", response_model=List[TurnResponse]) +def root(config: ConversationConfig): + """Submit your text here for conversational entity linking.""" + if DEBUG: + return [] + return config.response() + + +@app.post("/ne_concept", response_model=List[NEAnnotation]) +def root(config: NamedEntityConceptConfig): + """Submit your text here for conceptual entity disambiguation or linking.""" + if DEBUG: + return [] + return config.response() + if __name__ == "__main__": From a3a35a185f5a19d27d85da5985848ef14d60d403 Mon Sep 17 00:00:00 2001 From: Stef Smeets Date: Mon, 6 Feb 2023 11:31:29 +0100 Subject: [PATCH 09/14] Add schema examples --- src/REL/server.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/REL/server.py b/src/REL/server.py index 9d86eaf..25bd304 100644 --- a/src/REL/server.py +++ b/src/REL/server.py @@ -39,6 +39,15 @@ class NamedEntityConfig(BaseModel): "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] @@ -79,6 +88,27 @@ class ConversationConfig(BaseModel): "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"] From 7e5eb6c43963b1efc1484c7d4ac6b19d1fd60144 Mon Sep 17 00:00:00 2001 From: Stef Smeets Date: Mon, 6 Feb 2023 11:32:14 +0100 Subject: [PATCH 10/14] Rename function route `/` and `/ne/` to same function --- src/REL/server.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/REL/server.py b/src/REL/server.py index 25bd304..748e75f 100644 --- a/src/REL/server.py +++ b/src/REL/server.py @@ -165,10 +165,11 @@ def root(): @app.post("/", response_model=List[NEAnnotation]) -def root(config: NamedEntityConfig): +@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 endpoint. + + 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 @@ -179,16 +180,8 @@ def root(config: NamedEntityConfig): return config.response() -@app.post("/ne", response_model=List[NEAnnotation]) -def root(config: NamedEntityConfig): - """Submit your text here for entity disambiguation or linking.""" - if DEBUG: - return [] - return config.response() - - @app.post("/conv", response_model=List[TurnResponse]) -def root(config: ConversationConfig): +def conversational_entity_linking(config: ConversationConfig): """Submit your text here for conversational entity linking.""" if DEBUG: return [] @@ -196,7 +189,7 @@ def root(config: ConversationConfig): @app.post("/ne_concept", response_model=List[NEAnnotation]) -def root(config: NamedEntityConceptConfig): +def conceptual_named_entity_linking(config: NamedEntityConceptConfig): """Submit your text here for conceptual entity disambiguation or linking.""" if DEBUG: return [] From 6a8f11b24f28cbd7ce45a39b1592f2e6463827a4 Mon Sep 17 00:00:00 2001 From: Stef Smeets Date: Mon, 6 Feb 2023 13:36:18 +0100 Subject: [PATCH 11/14] Add more examples --- scripts/test_server.py | 4 +- src/REL/server.py | 84 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 70 insertions(+), 18 deletions(-) diff --git a/scripts/test_server.py b/scripts/test_server.py index be9c330..2d63b3d 100644 --- a/scripts/test_server.py +++ b/scripts/test_server.py @@ -85,8 +85,8 @@ ) for item in items: - endpoint = item['endpoint'] - payload = item['payload'] + endpoint = item["endpoint"] + payload = item["payload"] print("Request body:") print(payload) diff --git a/src/REL/server.py b/src/REL/server.py index 748e75f..7942dc3 100644 --- a/src/REL/server.py +++ b/src/REL/server.py @@ -13,10 +13,12 @@ Span = Tuple[int, int] + class NamedEntityConfig(BaseModel): """Config for named entity linking. For more information, see """ + text: str = Field(..., description="Text for entity linking or disambiguation.") spans: Optional[List[Span]] = Field( None, @@ -76,11 +78,20 @@ class ConversationTurn(BaseModel): ) 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): """Config for conversational entity linking. For more information: . """ + text: List[ConversationTurn] = Field( ..., description="Conversation as list of turns between two speakers." ) @@ -93,8 +104,8 @@ class Config: "example": { "text": ( { - "speaker": "USER", - "utterance": "I am allergic to tomatoes but we have a lot of famous Italian restaurants here in London.", + "speaker": "USER", + "utterance": "I am allergic to tomatoes but we have a lot of famous Italian restaurants here in London.", }, { "speaker": "SYSTEM", @@ -105,7 +116,7 @@ class Config: "utterance": "Talking of food, can you recommend me a restaurant in my city for our anniversary?", }, ), - "tagger": "default" + "tagger": "default", } } @@ -120,24 +131,51 @@ def response(self): class TurnAnnotation(BaseModel): __root__: List[Union[int, str]] = Field( ..., - min_items=4, max_items=4, + 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' + + speaker: str = "SYSTEM" + + class Config: + schema_extra = { + "example": { + "speaker": "SYSTEM", + "utterance": "Some people are allergic to histamine in tomatoes.", + }, + } + class UserResponse(ConversationTurn): """Return annotations when the speaker equals 'USER'.""" - speaker: str = 'USER' - annotations: List[TurnAnnotation] = Field( - ..., - description="List of annotations.") + + 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] @@ -145,16 +183,31 @@ class UserResponse(ConversationTurn): class NEAnnotation(BaseModel): """Annotation for named entity linking.""" - __root__: List[Union[int, str, float]] = Field(..., - min_items=7, max_items=7, + __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"] + } -@app.get("/") -def root(): +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, @@ -168,7 +221,7 @@ def root(): @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 @@ -196,7 +249,6 @@ def conceptual_named_entity_linking(config: NamedEntityConceptConfig): return config.response() - if __name__ == "__main__": import argparse import uvicorn From 2dfe75575ff9e0ae6a0665701056173b8795847f Mon Sep 17 00:00:00 2001 From: Stef Smeets Date: Mon, 6 Feb 2023 14:09:55 +0100 Subject: [PATCH 12/14] Fix link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 376b3a6..d84b775 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,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/) ## Efficiency of REL From 8645c13605415df2a249f181710a4727b8264814 Mon Sep 17 00:00:00 2001 From: Stef Smeets Date: Mon, 6 Feb 2023 14:39:00 +0100 Subject: [PATCH 13/14] Add some documentation for the server API --- docs/server_api.md | 221 +++++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 222 insertions(+) create mode 100644 docs/server_api.md 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/mkdocs.yml b/mkdocs.yml index f050fa6..bf4ab28 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,6 +2,7 @@ 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 From 4c38952c75effe6c71a9f22b05a72a83b7abcf47 Mon Sep 17 00:00:00 2001 From: Stef Smeets Date: Mon, 13 Feb 2023 11:01:55 +0100 Subject: [PATCH 14/14] Fix link --- docs/tutorials/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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/)