From f6f764c62465549c3287cb66e34ff49a918aff76 Mon Sep 17 00:00:00 2001 From: Shyam Upadhyay Date: Tue, 5 Feb 2019 11:18:25 -0500 Subject: [PATCH 01/17] how to have a multi_annotator with a single pipeline instance, rather than having N instances housed inside each annotator --- ccg_nlpy/server/annotator.py | 87 ++++++++++++++--- ccg_nlpy/server/example/dummy_annotator.py | 41 -------- ccg_nlpy/server/example/example_annotator.py | 41 ++++++++ ccg_nlpy/server/example/example_model.py | 24 +++++ .../example/example_model_wrapper_server.py | 30 ------ ccg_nlpy/server/example/example_server.py | 22 +++++ ccg_nlpy/server/multi_annotator.py | 96 +++++++++++++++++++ 7 files changed, 258 insertions(+), 83 deletions(-) delete mode 100644 ccg_nlpy/server/example/dummy_annotator.py create mode 100644 ccg_nlpy/server/example/example_annotator.py create mode 100644 ccg_nlpy/server/example/example_model.py delete mode 100644 ccg_nlpy/server/example/example_model_wrapper_server.py create mode 100644 ccg_nlpy/server/example/example_server.py create mode 100644 ccg_nlpy/server/multi_annotator.py diff --git a/ccg_nlpy/server/annotator.py b/ccg_nlpy/server/annotator.py index 6cc03d1..1055ef8 100644 --- a/ccg_nlpy/server/annotator.py +++ b/ccg_nlpy/server/annotator.py @@ -1,22 +1,46 @@ from typing import List - +from ccg_nlpy.pipeline_base import PipelineBase from ccg_nlpy.core.text_annotation import TextAnnotation +import json +from flask import request +import logging class Annotator: - def load_params(self) -> None: + def __init__(self, provided_view:str, required_views:List[str]): + # the viewname provided by the model + self.provided_view = provided_view + # the views required by the model (e.g. NER_CONLL for Wikifier) + self.required_views = required_views + # right now, we call the model load inside the init of server + # this could have been done outside. Cannot say which is a better choice. + # self.load_params() + # We need a pipeline to create views that are required by our model (e.g. NER is needed for WIKIFIER etc.) + self.pipeline = self.get_pipeline_instance() + logging.info("required views: %s", self.get_required_views()) + logging.info("provides view: %s", self.get_view_name()) + logging.info("ready!") + + # def load_params(self) -> None: + # """ + # Load the relevant model parameters. + # :return: None + # """ + # raise NotImplementedError + + def get_required_views(self) -> List[str]: """ - Load the relevant model parameters. - :return: None + The list of viewnames required by model (e.g. NER_CONLL is needed by Wikifier) + :return: list of viewnames """ - raise NotImplementedError + return self.required_views def get_view_name(self) -> str: """ - Return the name of the view that will be provided by the model. - :return: viewName + The viewname provided by model (e.g. NER_CONLL) + :return: viewname """ - raise NotImplementedError + return self.provided_view def add_view(self, docta: TextAnnotation) -> TextAnnotation: """ @@ -25,9 +49,48 @@ def add_view(self, docta: TextAnnotation) -> TextAnnotation: """ raise NotImplementedError - def get_required_views(self) -> List[str]: + def annotate(self) -> str: """ - Return the list of viewnames required by the model. - :return: List of view names + The method exposed through the flask interface. + :return: json of a text annotation """ - raise NotImplementedError \ No newline at end of file + # we get something like "?text=&views=". Below two lines extract these. + text = request.args.get('text') + views = request.args.get('views') + logging.info("request args views:%s", views) + if text is None or views is None: + return "The parameters 'text' and/or 'views' are not specified. Here is a sample input: ?text=\"This is a " \ + "sample sentence. I'm happy.\"&views=POS,NER " + views = views.split(",") + if self.provided_view not in views: + logging.info("desired view not provided by this server.") + # After discussing with Daniel, this is the proper discipline to handle views not provided by this. + # The appelles server will fallback to the next remote server. + return "VIEW NOT PROVIDED" + + # create a text ann with the required views for the model + docta = self.get_text_annotation_for_model(text=text, required_views=self.get_required_views()) + + # send it to your model for inference + docta = self.add_view(docta=docta) + + # make the returned text ann to a json + ta_json = json.dumps(docta.as_json) + + return ta_json + + def get_pipeline_instance(self) -> PipelineBase: + """ + Creates a pipeline instance (either local or remote pipeline) to get the views required by the model. + :return: PipelineBase object (LocalPipeline or RemotePipeline) + """ + raise NotImplementedError + + def get_text_annotation_for_model(self, text: str, required_views: List[str]) -> TextAnnotation: + """ + This takes text from the annotate api call and creates a text annotation with the views required by the model. + :param text: text from the demo interface, coming through the annotate request call + :param required_views: views required by the model + :return: text annotation, to be sent to the model's inference on ta method + """ + raise NotImplementedError diff --git a/ccg_nlpy/server/example/dummy_annotator.py b/ccg_nlpy/server/example/dummy_annotator.py deleted file mode 100644 index 1b97865..0000000 --- a/ccg_nlpy/server/example/dummy_annotator.py +++ /dev/null @@ -1,41 +0,0 @@ -import logging -from typing import List - -from ccg_nlpy.server.annotator import Annotator - -logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO) -import copy - - -# A dummy model that is used with the model wrapper server You need to define two methods load_params and -# inference_on_ta when writing your own model, for it to be compatible with the model wrapper server. -class DummyAnnotator(Annotator): - def __init__(self): - self.provided_view = "DUMMYVIEW" - # self.required_views = ["TOKENS", "NER_CONLL"] - self.required_views = ["TOKENS"] - - def get_required_views(self) -> List[str]: - return self.required_views - - def get_view_name(self) -> str: - return self.provided_view - - def load_params(self): - logging.info("loading model params ...") - - def add_view(self, docta): - # This upcases each token. Test for TokenLabelView - new_view = copy.deepcopy(docta.get_view("TOKENS")) - tokens = docta.get_tokens - for token, cons in zip(tokens, new_view.cons_list): - cons["label"] = token.upper() - - # # This replaces each NER with its upcased tokens. Test for SpanLabelView - # new_view = copy.deepcopy(docta.get_view("NER_CONLL")) - # for nercons in new_view.cons_list: - # nercons["label"] = nercons["tokens"].upper() - - new_view.view_name = self.provided_view - docta.view_dictionary[self.provided_view] = new_view - return docta diff --git a/ccg_nlpy/server/example/example_annotator.py b/ccg_nlpy/server/example/example_annotator.py new file mode 100644 index 0000000..7bd1a25 --- /dev/null +++ b/ccg_nlpy/server/example/example_annotator.py @@ -0,0 +1,41 @@ +import logging +from typing import List + +from ccg_nlpy import local_pipeline, TextAnnotation +from ccg_nlpy.pipeline_base import PipelineBase +from ccg_nlpy.server.annotator import Annotator + +logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO) + + +class ExampleAnnotator(Annotator): + """ + A dummy model that is used with the model wrapper server You need to define two methods load_params and + inference_on_ta when writing your own model, for it to be compatible with the model wrapper server. + """ + def __init__(self, model, provided_view: str, required_views: List[str]): + super().__init__(provided_view, required_views) + self.model = model + + # def load_params(self): + # logging.info("loading model params ...") + # raise NotImplementedError + + def add_view(self, docta): + new_view = self.model.get_view_from_text_annotation(docta) + + new_view.view_name = self.provided_view + docta.view_dictionary[self.provided_view] = new_view + return docta + + def get_pipeline_instance(self) -> PipelineBase: + return local_pipeline.LocalPipeline() + + def get_text_annotation_for_model(self, text: str, required_views: List[str]): + # TODO This is a problem with ccg_nlpy text annotation, it does not like newlines (e.g., marking paragraphs) + text = text.replace("\n", "") + pretokenized_text = [text.split(" ")] + required_views = ",".join(required_views) + ta_json = self.pipeline.call_server_pretokenized(pretokenized_text=pretokenized_text, views=required_views) + ta = TextAnnotation(json_str=ta_json) + return ta diff --git a/ccg_nlpy/server/example/example_model.py b/ccg_nlpy/server/example/example_model.py new file mode 100644 index 0000000..c2eeff7 --- /dev/null +++ b/ccg_nlpy/server/example/example_model.py @@ -0,0 +1,24 @@ +from ccg_nlpy import TextAnnotation +import copy + +from ccg_nlpy.core.view import View + + +class ExampleModel: + """This would be your pytorch/dynet/tensorflow model""" + def __init__(self): + pass + + def get_view_from_model(self, docta:TextAnnotation) -> View: + # This upcases each token. Test for TokenLabelView + new_view = copy.deepcopy(docta.get_view("TOKENS")) + tokens = docta.get_tokens + for token, cons in zip(tokens, new_view.cons_list): + cons["label"] = token.upper() + + # # This replaces each NER with its upcased tokens. Test for SpanLabelView + # new_view = copy.deepcopy(docta.get_view("NER_CONLL")) + # for nercons in new_view.cons_list: + # nercons["label"] = nercons["tokens"].upper() + + return new_view \ No newline at end of file diff --git a/ccg_nlpy/server/example/example_model_wrapper_server.py b/ccg_nlpy/server/example/example_model_wrapper_server.py deleted file mode 100644 index 7652e59..0000000 --- a/ccg_nlpy/server/example/example_model_wrapper_server.py +++ /dev/null @@ -1,30 +0,0 @@ -from flask import Flask -from flask_cors import CORS - -from ccg_nlpy.server.example.dummy_annotator import DummyAnnotator -from ccg_nlpy.server.model_wrapper_server_local_pipeline import ModelWrapperServerLocal -from ccg_nlpy.server.model_wrapper_server_remote_pipeline import ModelWrapperServerRemote - -app = Flask(__name__) -# necessary for testing on localhost -CORS(app) - - -def main(): - model = DummyAnnotator() # create your model object here, see the DummyModel class for a minimal example. - - # here the local pipeline is used to create the initial text annotation, best for pretokenized cases, like non-English - wrapper = ModelWrapperServerLocal(model=model) - - # here the remote pipeline is used to create the initial text annotation, best for English handling demos - wrapper = ModelWrapperServerRemote(model=model) - - # Expose wrapper.annotate method through a Flask server - app.add_url_rule(rule='/annotate', endpoint='annotate', view_func=wrapper.annotate, methods=['GET']) - app.run(host='0.0.0.0', port=4003) - # On running this main(), you should be able to visit the following URL and see a json text annotation returned - # http://127.0.0.1:5000/annotate?text="Stephen Mayhew is a person's name"&views=DUMMYVIEW - - -if __name__ == "__main__": - main() diff --git a/ccg_nlpy/server/example/example_server.py b/ccg_nlpy/server/example/example_server.py new file mode 100644 index 0000000..cfb76b5 --- /dev/null +++ b/ccg_nlpy/server/example/example_server.py @@ -0,0 +1,22 @@ +from ccg_nlpy.server.example.example_annotator import ExampleAnnotator +from flask import Flask +from flask_cors import CORS + +app = Flask(__name__) +# necessary for testing on localhost +CORS(app) + + +def main(): + # create your model object here, see the DummyModel class for a minimal example. + annotator = ExampleAnnotator(model=mymodel, provided_view="DUMMYVIEW", required_views=["TOKENS"]) + + # Expose wrapper.annotate method through a Flask server + app.add_url_rule(rule='/annotate', endpoint='annotate', view_func=annotator.annotate, methods=['GET']) + app.run(host='localhost', port=5000) + # On running this main(), you should be able to visit the following URL and see a json text annotation returned + # http://127.0.0.1:5000/annotate?text="Stephen Mayhew is a person's name"&views=DUMMYVIEW + + +if __name__ == "__main__": + main() diff --git a/ccg_nlpy/server/multi_annotator.py b/ccg_nlpy/server/multi_annotator.py new file mode 100644 index 0000000..007d604 --- /dev/null +++ b/ccg_nlpy/server/multi_annotator.py @@ -0,0 +1,96 @@ +from typing import List +from ccg_nlpy.pipeline_base import PipelineBase +from ccg_nlpy.core.text_annotation import TextAnnotation +import json + +from ccg_nlpy.server.annotator import Annotator +from flask import request +import logging + + +class MultiAnnotator: + def __init__(self, annotators: List[Annotator]): + self.annotators = annotators + # all models should have the same set of required views + self.required_views = annotators[0].get_required_views() + self.provided_views = [m.get_view_name() for m in annotators] + print("provided views", self.provided_views) + + # for each viewname (e.g. POS_Arabic) know which model to call (Arabic_POS_Tagger) + self.view2annotator_dict = {} + for m in self.annotators: + self.view2annotator_dict[m.get_view_name()] = m + + logging.info("required views: %s", self.required_views) + logging.info("provided views: %s", self.provided_views) + logging.info("ready!") + + # def load_params(self) -> None: + # """ + # Load the relevant model parameters. + # :return: None + # """ + # raise NotImplementedError + + def get_required_views(self) -> List[str]: + """ + The list of viewnames required by model (e.g. NER_CONLL is needed by Wikifier) + :return: list of viewnames + """ + return self.required_views + + def get_view_names(self) -> List[str]: + """ + The list of viewnames provided by model (e.g. [NER_CONLL, NER_Ontonotes] or [POS_English, POS_French, POS_ ...]) + :return: list of viewnames + """ + return self.provided_views + + def add_view(self, docta: TextAnnotation) -> TextAnnotation: + """ + Takes a text annotation and adds the view provided by this model to it. + :return: TextAnnotation + """ + raise NotImplementedError + + def annotate(self): + # we get something like "?text=&views=". Below two lines extract these. + text = request.args.get('text') + views = request.args.get('views') + logging.info("request args views:%s", views) + if text is None or views is None: + return "The parameters 'text' and/or 'views' are not specified. Here is a sample input: ?text=\"This is a " \ + "sample sentence. I'm happy.\"&views=POS,NER " + views = views.split(",") + + for view in views: + if view in self.provided_views: + + # select the correct model + relevant_annotator = self.view2annotator_dict[view] + # create a text ann with the required views for the model + docta = self.get_text_annotation_for_model(text=text, required_views=self.required_views) + # send it to your model for inference + docta = relevant_annotator.add_view(docta=docta) + # make the returned text ann to a json + ta_json = json.dumps(docta.as_json) + # print("returning", ta_json) + return ta_json + # If we reached here, it means the requested view cannot be provided by this annotator + return "VIEW NOT PROVIDED" + + def get_pipeline_instance(self) -> PipelineBase: + """ + Creates a pipeline instance (either local or remote pipeline) to get the views required by the model. + :return: PipelineBase object (LocalPipeline or RemotePipeline) + """ + raise NotImplementedError + + def get_text_annotation_for_model(self, text: str, required_views: List[str]) -> TextAnnotation: + """ + This takes text from the annotate api call and creates a text annotation with the views required by the model. + :param text: text from the demo interface, coming through the annotate request call + :param required_views: views required by the model + :return: text annotation, to be sent to the model's inference on ta method + """ + raise NotImplementedError From 862fe01d6845dcd642c8f31f428b15d4e854ca63 Mon Sep 17 00:00:00 2001 From: Shyam Upadhyay Date: Tue, 5 Feb 2019 12:23:09 -0500 Subject: [PATCH 02/17] working with translit demo --- ccg_nlpy/server/annotator.py | 18 ++----------- ccg_nlpy/server/example/example_annotator.py | 7 ++--- ccg_nlpy/server/example/example_server.py | 6 ++++- ccg_nlpy/server/multi_annotator.py | 27 ++------------------ 4 files changed, 11 insertions(+), 47 deletions(-) diff --git a/ccg_nlpy/server/annotator.py b/ccg_nlpy/server/annotator.py index 1055ef8..eaa3763 100644 --- a/ccg_nlpy/server/annotator.py +++ b/ccg_nlpy/server/annotator.py @@ -7,7 +7,7 @@ class Annotator: - def __init__(self, provided_view:str, required_views:List[str]): + def __init__(self, pipeline: PipelineBase, provided_view:str, required_views:List[str]): # the viewname provided by the model self.provided_view = provided_view # the views required by the model (e.g. NER_CONLL for Wikifier) @@ -16,18 +16,11 @@ def __init__(self, provided_view:str, required_views:List[str]): # this could have been done outside. Cannot say which is a better choice. # self.load_params() # We need a pipeline to create views that are required by our model (e.g. NER is needed for WIKIFIER etc.) - self.pipeline = self.get_pipeline_instance() + self.pipeline = pipeline logging.info("required views: %s", self.get_required_views()) logging.info("provides view: %s", self.get_view_name()) logging.info("ready!") - # def load_params(self) -> None: - # """ - # Load the relevant model parameters. - # :return: None - # """ - # raise NotImplementedError - def get_required_views(self) -> List[str]: """ The list of viewnames required by model (e.g. NER_CONLL is needed by Wikifier) @@ -79,13 +72,6 @@ def annotate(self) -> str: return ta_json - def get_pipeline_instance(self) -> PipelineBase: - """ - Creates a pipeline instance (either local or remote pipeline) to get the views required by the model. - :return: PipelineBase object (LocalPipeline or RemotePipeline) - """ - raise NotImplementedError - def get_text_annotation_for_model(self, text: str, required_views: List[str]) -> TextAnnotation: """ This takes text from the annotate api call and creates a text annotation with the views required by the model. diff --git a/ccg_nlpy/server/example/example_annotator.py b/ccg_nlpy/server/example/example_annotator.py index 7bd1a25..976ee4d 100644 --- a/ccg_nlpy/server/example/example_annotator.py +++ b/ccg_nlpy/server/example/example_annotator.py @@ -13,8 +13,8 @@ class ExampleAnnotator(Annotator): A dummy model that is used with the model wrapper server You need to define two methods load_params and inference_on_ta when writing your own model, for it to be compatible with the model wrapper server. """ - def __init__(self, model, provided_view: str, required_views: List[str]): - super().__init__(provided_view, required_views) + def __init__(self, model, pipeline: PipelineBase, provided_view: str, required_views: List[str]): + super().__init__(pipeline=pipeline, provided_view=provided_view, required_views=required_views) self.model = model # def load_params(self): @@ -28,9 +28,6 @@ def add_view(self, docta): docta.view_dictionary[self.provided_view] = new_view return docta - def get_pipeline_instance(self) -> PipelineBase: - return local_pipeline.LocalPipeline() - def get_text_annotation_for_model(self, text: str, required_views: List[str]): # TODO This is a problem with ccg_nlpy text annotation, it does not like newlines (e.g., marking paragraphs) text = text.replace("\n", "") diff --git a/ccg_nlpy/server/example/example_server.py b/ccg_nlpy/server/example/example_server.py index cfb76b5..05b3aa5 100644 --- a/ccg_nlpy/server/example/example_server.py +++ b/ccg_nlpy/server/example/example_server.py @@ -1,4 +1,6 @@ +from ccg_nlpy import local_pipeline from ccg_nlpy.server.example.example_annotator import ExampleAnnotator +from ccg_nlpy.server.example.example_model import ExampleModel from flask import Flask from flask_cors import CORS @@ -9,7 +11,9 @@ def main(): # create your model object here, see the DummyModel class for a minimal example. - annotator = ExampleAnnotator(model=mymodel, provided_view="DUMMYVIEW", required_views=["TOKENS"]) + mymodel = ExampleModel() + pipeline = local_pipeline.LocalPipeline() + annotator = ExampleAnnotator(model=mymodel, pipeline=pipeline, provided_view="DUMMYVIEW", required_views=["TOKENS"]) # Expose wrapper.annotate method through a Flask server app.add_url_rule(rule='/annotate', endpoint='annotate', view_func=annotator.annotate, methods=['GET']) diff --git a/ccg_nlpy/server/multi_annotator.py b/ccg_nlpy/server/multi_annotator.py index 007d604..c41806c 100644 --- a/ccg_nlpy/server/multi_annotator.py +++ b/ccg_nlpy/server/multi_annotator.py @@ -25,13 +25,6 @@ def __init__(self, annotators: List[Annotator]): logging.info("provided views: %s", self.provided_views) logging.info("ready!") - # def load_params(self) -> None: - # """ - # Load the relevant model parameters. - # :return: None - # """ - # raise NotImplementedError - def get_required_views(self) -> List[str]: """ The list of viewnames required by model (e.g. NER_CONLL is needed by Wikifier) @@ -67,9 +60,9 @@ def annotate(self): if view in self.provided_views: # select the correct model - relevant_annotator = self.view2annotator_dict[view] + relevant_annotator: Annotator = self.view2annotator_dict[view] # create a text ann with the required views for the model - docta = self.get_text_annotation_for_model(text=text, required_views=self.required_views) + docta = relevant_annotator.get_text_annotation_for_model(text=text, required_views=self.required_views) # send it to your model for inference docta = relevant_annotator.add_view(docta=docta) # make the returned text ann to a json @@ -78,19 +71,3 @@ def annotate(self): return ta_json # If we reached here, it means the requested view cannot be provided by this annotator return "VIEW NOT PROVIDED" - - def get_pipeline_instance(self) -> PipelineBase: - """ - Creates a pipeline instance (either local or remote pipeline) to get the views required by the model. - :return: PipelineBase object (LocalPipeline or RemotePipeline) - """ - raise NotImplementedError - - def get_text_annotation_for_model(self, text: str, required_views: List[str]) -> TextAnnotation: - """ - This takes text from the annotate api call and creates a text annotation with the views required by the model. - :param text: text from the demo interface, coming through the annotate request call - :param required_views: views required by the model - :return: text annotation, to be sent to the model's inference on ta method - """ - raise NotImplementedError From 208cf5c00b477f6d0e84ea06383893772ada3022 Mon Sep 17 00:00:00 2001 From: Shyam Upadhyay Date: Tue, 5 Feb 2019 12:54:22 -0500 Subject: [PATCH 03/17] rm'ed wrapper servers --- .cache/v/cache/lastfailed | 4 + .pytest_cache/README.md | 8 ++ .pytest_cache/v/cache/lastfailed | 1 + .pytest_cache/v/cache/nodeids | 29 ++++++ ccg_nlpy/server/model_wrapper_server.py | 99 ------------------- .../model_wrapper_server_local_pipeline.py | 31 ------ .../model_wrapper_server_remote_pipeline.py | 30 ------ ccg_nlpy/server/multi_model_wrapper_server.py | 88 ----------------- ...lti_model_wrapper_server_local_pipeline.py | 30 ------ 9 files changed, 42 insertions(+), 278 deletions(-) create mode 100644 .cache/v/cache/lastfailed create mode 100644 .pytest_cache/README.md create mode 100644 .pytest_cache/v/cache/lastfailed create mode 100644 .pytest_cache/v/cache/nodeids delete mode 100644 ccg_nlpy/server/model_wrapper_server.py delete mode 100644 ccg_nlpy/server/model_wrapper_server_local_pipeline.py delete mode 100644 ccg_nlpy/server/model_wrapper_server_remote_pipeline.py delete mode 100644 ccg_nlpy/server/multi_model_wrapper_server.py delete mode 100644 ccg_nlpy/server/multi_model_wrapper_server_local_pipeline.py diff --git a/.cache/v/cache/lastfailed b/.cache/v/cache/lastfailed new file mode 100644 index 0000000..b53b7c8 --- /dev/null +++ b/.cache/v/cache/lastfailed @@ -0,0 +1,4 @@ +{ + "tests/test_pipeline_config.py::TestPipelineConfig::test_get_current_config_without_models": true, + "tests/test_pipeline_config.py::TestPipelineConfig::test_get_user_config": true +} \ No newline at end of file diff --git a/.pytest_cache/README.md b/.pytest_cache/README.md new file mode 100644 index 0000000..bb78ba0 --- /dev/null +++ b/.pytest_cache/README.md @@ -0,0 +1,8 @@ +# pytest cache directory # + +This directory contains data from the pytest's cache plugin, +which provides the `--lf` and `--ff` options, as well as the `cache` fixture. + +**Do not** commit this to version control. + +See [the docs](https://docs.pytest.org/en/latest/cache.html) for more information. diff --git a/.pytest_cache/v/cache/lastfailed b/.pytest_cache/v/cache/lastfailed new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.pytest_cache/v/cache/lastfailed @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.pytest_cache/v/cache/nodeids b/.pytest_cache/v/cache/nodeids new file mode 100644 index 0000000..c57053d --- /dev/null +++ b/.pytest_cache/v/cache/nodeids @@ -0,0 +1,29 @@ +[ + "tests/test_download.py::TestDownload::test_recover_model_config", + "tests/test_local_pipeline.py::TestLocalPipeline::test_doc", + "tests/test_local_pipeline.py::TestLocalPipeline::test_doc_illigal_characters", + "tests/test_local_pipeline.py::TestLocalPipeline::test_doc_pretokenized", + "tests/test_local_pipeline.py::TestLocalPipeline::test_split_on_hyphens", + "tests/test_local_pipeline.py::TestLocalPipeline::test_unicode", + "tests/test_pipeline_base.py::TestPipelineBase::test_user_config", + "tests/test_pipeline_config.py::TestPipelineConfig::test_change_temporary_config", + "tests/test_pipeline_config.py::TestPipelineConfig::test_get_current_config_with_models", + "tests/test_pipeline_config.py::TestPipelineConfig::test_get_current_config_without_models", + "tests/test_pipeline_config.py::TestPipelineConfig::test_get_user_config", + "tests/test_remote_pipeline.py::TestRemotePipeline::test_doc", + "tests/test_remote_pipeline.py::TestRemotePipeline::test_doc_illigal_characters", + "tests/test_remote_pipeline.py::TestRemotePipeline::test_status_code", + "tests/test_serialization.py::TestView::test_print_view", + "tests/test_text_annotation.py::TestTextAnnotation::test_end_pos", + "tests/test_text_annotation.py::TestTextAnnotation::test_get_views", + "tests/test_text_annotation.py::TestTextAnnotation::test_invalid_constituents", + "tests/test_text_annotation.py::TestTextAnnotation::test_score", + "tests/test_text_annotation.py::TestTextAnnotation::test_sent_boundaries", + "tests/test_text_annotation.py::TestTextAnnotation::test_text", + "tests/test_text_annotation.py::TestTextAnnotation::test_tokens", + "tests/test_view.py::TestView::test_get_con_with_different_keys", + "tests/test_view.py::TestView::test_overlapping_span", + "tests/test_view.py::TestView::test_print_view", + "tests/test_view.py::TestView::test_relations", + "tests/test_view.py::TestView::test_view_type" +] \ No newline at end of file diff --git a/ccg_nlpy/server/model_wrapper_server.py b/ccg_nlpy/server/model_wrapper_server.py deleted file mode 100644 index bcab0ca..0000000 --- a/ccg_nlpy/server/model_wrapper_server.py +++ /dev/null @@ -1,99 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import logging -from typing import List - -from ccg_nlpy.pipeline_base import PipelineBase - -from ccg_nlpy.server.annotator import Annotator - -logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO) - -from ccg_nlpy.core.text_annotation import TextAnnotation -import json -from flask import request - - -class ModelWrapperServer: - """ - A simple interface to serve your python models through visualization tools like apelles that consume text - annotations. This wraps around a model that can add a new view to a text annotation, and serves it using a flask - server. - """ - - def __init__(self, model: Annotator): - self.model = model - # the viewname provided by the model - self.provided_view = model.get_view_name() - # the views required by the model (e.g. NER_CONLL for Wikifier) - self.required_views = model.get_required_views() - # right now, we call the model load inside the init of server - # this could have been done outside. Cannot say which is a better choice. - self.model.load_params() - # We need a pipeline to create views that are required by our model (e.g. NER is needed for WIKIFIER etc.) - self.pipeline = self.get_pipeline_instance() - logging.info("required views: %s", self.get_required_views()) - logging.info("provides view: %s", self.get_view_name()) - logging.info("ready!") - - def get_required_views(self) -> List[str]: - """ - The list of viewnames required by model (e.g. NER_CONLL is needed by Wikifier) - :return: list of viewnames - """ - return self.required_views - - def get_view_name(self) -> str: - """ - The viewname provided by model (e.g. NER_CONLL) - :return: viewname - """ - return self.provided_view - - def annotate(self) -> str: - """ - The method exposed through the flask interface. - :return: json of a text annotation - """ - # we get something like "?text=&views=". Below two lines extract these. - text = request.args.get('text') - views = request.args.get('views') - logging.info("request args views:%s", views) - if text is None or views is None: - return "The parameters 'text' and/or 'views' are not specified. Here is a sample input: ?text=\"This is a " \ - "sample sentence. I'm happy.\"&views=POS,NER " - views = views.split(",") - if self.provided_view not in views: - logging.info("desired view not provided by this server.") - # After discussing with Daniel, this is the proper discipline to handle views not provided by this. - # The appelles server will fallback to the next remote server. - return "VIEW NOT PROVIDED" - - # create a text ann with the required views for the model - docta = self.get_text_annotation_for_model(text=text, required_views=self.get_required_views()) - - # send it to your model for inference - docta = self.model.add_view(docta=docta) - - # make the returned text ann to a json - ta_json = json.dumps(docta.as_json) - - return ta_json - - def get_pipeline_instance(self) -> PipelineBase: - """ - Creates a pipeline instance (either local or remote pipeline) to get the views required by the model. - :return: PipelineBase object (LocalPipeline or RemotePipeline) - """ - raise NotImplementedError - - def get_text_annotation_for_model(self, text: str, required_views: List[str]) -> TextAnnotation: - """ - This takes text from the annotate api call and creates a text annotation with the views required by the model. - :param text: text from the demo interface, coming through the annotate request call - :param required_views: views required by the model - :return: text annotation, to be sent to the model's inference on ta method - """ - raise NotImplementedError diff --git a/ccg_nlpy/server/model_wrapper_server_local_pipeline.py b/ccg_nlpy/server/model_wrapper_server_local_pipeline.py deleted file mode 100644 index 9394b7c..0000000 --- a/ccg_nlpy/server/model_wrapper_server_local_pipeline.py +++ /dev/null @@ -1,31 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import logging -from typing import List - -from ccg_nlpy.server.annotator import Annotator -from ccg_nlpy.server.model_wrapper_server import ModelWrapperServer - -logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO) - -from ccg_nlpy import local_pipeline -from ccg_nlpy.core.text_annotation import TextAnnotation - - -class ModelWrapperServerLocal(ModelWrapperServer): - def __init__(self, model: Annotator): - super().__init__(model) - - def get_pipeline_instance(self): - return local_pipeline.LocalPipeline() - - def get_text_annotation_for_model(self, text: str, required_views: List[str]): - # TODO This is a problem with ccg_nlpy text annotation, it does not like newlines (e.g., marking paragraphs) - text = text.replace("\n", "") - pretokenized_text = [text.split(" ")] - required_views = ",".join(required_views) - ta_json = self.pipeline.call_server_pretokenized(pretokenized_text=pretokenized_text, views=required_views) - ta = TextAnnotation(json_str=ta_json) - return ta diff --git a/ccg_nlpy/server/model_wrapper_server_remote_pipeline.py b/ccg_nlpy/server/model_wrapper_server_remote_pipeline.py deleted file mode 100644 index 87ede66..0000000 --- a/ccg_nlpy/server/model_wrapper_server_remote_pipeline.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import logging -from typing import List - -from ccg_nlpy.server.annotator import Annotator -from ccg_nlpy.server.model_wrapper_server import ModelWrapperServer - -logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO) - -from ccg_nlpy import remote_pipeline -from ccg_nlpy.core.text_annotation import TextAnnotation - - -class ModelWrapperServerRemote(ModelWrapperServer): - def __init__(self, model: Annotator): - super().__init__(model) - - def get_pipeline_instance(self): - return remote_pipeline.RemotePipeline() - - def get_text_annotation_for_model(self, text: str, required_views: List[str]): - # TODO This is a problem with ccg_nlpy text annotation, it does not like newlines (e.g., marking paragraphs) - text = text.replace("\n", "") - required_views = ",".join(required_views) - ta_json = self.pipeline.call_server(text=text, views=required_views) - ta = TextAnnotation(json_str=ta_json) - return ta diff --git a/ccg_nlpy/server/multi_model_wrapper_server.py b/ccg_nlpy/server/multi_model_wrapper_server.py deleted file mode 100644 index d940e66..0000000 --- a/ccg_nlpy/server/multi_model_wrapper_server.py +++ /dev/null @@ -1,88 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import logging -from typing import List -from ccg_nlpy.pipeline_base import PipelineBase - -from ccg_nlpy.server.annotator import Annotator - -logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO) - -from ccg_nlpy.core.text_annotation import TextAnnotation -import json -from flask import request - - -class MultiModelWrapperServer: - """ - Multilingual counterpart of ModelWrapper Server. Use this to serve multiple related models. - For example, you can serve a POS tagger in languages A,B,C... simultaneously using this wrapper. - """ - - def __init__(self, models: List[Annotator]): - self.models = models - # all models should have the same set of required views - self.required_views = models[0].get_required_views() - self.provided_views = [m.get_view_name() for m in models] - print("provided views", self.provided_views) - - # for each viewname (e.g. POS_Arabic) know which model to call (Arabic_POS_Tagger) - self.view2model_dict = {} - for m in self.models: - m.load_params() - self.view2model_dict[m.get_view_name()] = m - - # We need a pipeline to create views that are required by our model (e.g. NER is needed for WIKIFIER etc.) - self.pipeline = self.get_pipeline_instance() - logging.info("required views: %s", self.get_required_views()) - logging.info("provided views: %s", self.get_provided_views()) - logging.info("ready!") - - def get_required_views(self) -> List[str]: - return self.required_views - - def get_provided_views(self) -> List[str]: - return self.provided_views - - def annotate(self): - # we get something like "?text=&views=". Below two lines extract these. - text = request.args.get('text') - views = request.args.get('views') - logging.info("request args views:%s", views) - if text is None or views is None: - return "The parameters 'text' and/or 'views' are not specified. Here is a sample input: ?text=\"This is a " \ - "sample sentence. I'm happy.\"&views=POS,NER " - views = views.split(",") - - for view in views: - if view in self.provided_views: - # create a text ann with the required views for the model - docta = self.get_text_annotation_for_model(text=text, required_views=self.required_views) - # select the correct model - relevant_model = self.view2model_dict[view] - # send it to your model for inference - docta = relevant_model.add_view(docta=docta) - # make the returned text ann to a json - ta_json = json.dumps(docta.as_json) - # print("returning", ta_json) - return ta_json - # If we reached here, it means the requested view cannot be provided by this annotator - return "VIEW NOT PROVIDED" - - def get_pipeline_instance(self) -> PipelineBase: - """ - Creates a pipeline instance (either local or remote pipeline) to get the views required by the model. - :return: PipelineBase object (LocalPipeline or RemotePipeline) - """ - raise NotImplementedError - - def get_text_annotation_for_model(self, text: str, required_views: List[str]) -> TextAnnotation: - """ - This takes text from the annotate api call and creates a text annotation with the views required by the model. - :param text: text from the demo interface, coming through the annotate request call - :param required_views: views required by the model - :return: text annotation, to be sent to the model's inference on ta method - """ - raise NotImplementedError diff --git a/ccg_nlpy/server/multi_model_wrapper_server_local_pipeline.py b/ccg_nlpy/server/multi_model_wrapper_server_local_pipeline.py deleted file mode 100644 index 4322980..0000000 --- a/ccg_nlpy/server/multi_model_wrapper_server_local_pipeline.py +++ /dev/null @@ -1,30 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -import logging - -from ccg_nlpy.server.multi_model_wrapper_server import MultiModelWrapperServer - -logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO) - -from ccg_nlpy import local_pipeline -from ccg_nlpy.core.text_annotation import TextAnnotation - - -class MultiModelWrapperServerLocal(MultiModelWrapperServer): - def __init__(self, models): - super().__init__(models) - - def get_pipeline_instance(self): - return local_pipeline.LocalPipeline() - - def get_text_annotation_for_model(self, text, required_views): - # TODO This is a problem with ccg_nlpy text annotation, it does not like newlines (e.g., marking paragraphs) - text = text.replace("\n", "") - pretokenized_text = [text.split(" ")] - required_views = ",".join(required_views) - logging.info(f"required_views:{required_views}") - ta_json = self.pipeline.call_server_pretokenized(pretokenized_text=pretokenized_text, views=required_views) - ta = TextAnnotation(json_str=ta_json) - return ta From 282fa0021e133d37fde91013b75084df09ab4742 Mon Sep 17 00:00:00 2001 From: Shyam Upadhyay Date: Tue, 5 Feb 2019 12:56:15 -0500 Subject: [PATCH 04/17] rm'ed accidently added cache files. Also modifying the .gitignore so this does not happen --- .cache/v/cache/lastfailed | 4 ---- .gitignore | 3 +++ .pytest_cache/README.md | 8 -------- .pytest_cache/v/cache/lastfailed | 1 - .pytest_cache/v/cache/nodeids | 29 ----------------------------- 5 files changed, 3 insertions(+), 42 deletions(-) delete mode 100644 .cache/v/cache/lastfailed delete mode 100644 .pytest_cache/README.md delete mode 100644 .pytest_cache/v/cache/lastfailed delete mode 100644 .pytest_cache/v/cache/nodeids diff --git a/.cache/v/cache/lastfailed b/.cache/v/cache/lastfailed deleted file mode 100644 index b53b7c8..0000000 --- a/.cache/v/cache/lastfailed +++ /dev/null @@ -1,4 +0,0 @@ -{ - "tests/test_pipeline_config.py::TestPipelineConfig::test_get_current_config_without_models": true, - "tests/test_pipeline_config.py::TestPipelineConfig::test_get_user_config": true -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 64d4625..77ee723 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ html/ # Python egg metadata, regenerated from source files by setuptools. /*.egg-info + +.cache +.pytest_cache \ No newline at end of file diff --git a/.pytest_cache/README.md b/.pytest_cache/README.md deleted file mode 100644 index bb78ba0..0000000 --- a/.pytest_cache/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# pytest cache directory # - -This directory contains data from the pytest's cache plugin, -which provides the `--lf` and `--ff` options, as well as the `cache` fixture. - -**Do not** commit this to version control. - -See [the docs](https://docs.pytest.org/en/latest/cache.html) for more information. diff --git a/.pytest_cache/v/cache/lastfailed b/.pytest_cache/v/cache/lastfailed deleted file mode 100644 index 9e26dfe..0000000 --- a/.pytest_cache/v/cache/lastfailed +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/.pytest_cache/v/cache/nodeids b/.pytest_cache/v/cache/nodeids deleted file mode 100644 index c57053d..0000000 --- a/.pytest_cache/v/cache/nodeids +++ /dev/null @@ -1,29 +0,0 @@ -[ - "tests/test_download.py::TestDownload::test_recover_model_config", - "tests/test_local_pipeline.py::TestLocalPipeline::test_doc", - "tests/test_local_pipeline.py::TestLocalPipeline::test_doc_illigal_characters", - "tests/test_local_pipeline.py::TestLocalPipeline::test_doc_pretokenized", - "tests/test_local_pipeline.py::TestLocalPipeline::test_split_on_hyphens", - "tests/test_local_pipeline.py::TestLocalPipeline::test_unicode", - "tests/test_pipeline_base.py::TestPipelineBase::test_user_config", - "tests/test_pipeline_config.py::TestPipelineConfig::test_change_temporary_config", - "tests/test_pipeline_config.py::TestPipelineConfig::test_get_current_config_with_models", - "tests/test_pipeline_config.py::TestPipelineConfig::test_get_current_config_without_models", - "tests/test_pipeline_config.py::TestPipelineConfig::test_get_user_config", - "tests/test_remote_pipeline.py::TestRemotePipeline::test_doc", - "tests/test_remote_pipeline.py::TestRemotePipeline::test_doc_illigal_characters", - "tests/test_remote_pipeline.py::TestRemotePipeline::test_status_code", - "tests/test_serialization.py::TestView::test_print_view", - "tests/test_text_annotation.py::TestTextAnnotation::test_end_pos", - "tests/test_text_annotation.py::TestTextAnnotation::test_get_views", - "tests/test_text_annotation.py::TestTextAnnotation::test_invalid_constituents", - "tests/test_text_annotation.py::TestTextAnnotation::test_score", - "tests/test_text_annotation.py::TestTextAnnotation::test_sent_boundaries", - "tests/test_text_annotation.py::TestTextAnnotation::test_text", - "tests/test_text_annotation.py::TestTextAnnotation::test_tokens", - "tests/test_view.py::TestView::test_get_con_with_different_keys", - "tests/test_view.py::TestView::test_overlapping_span", - "tests/test_view.py::TestView::test_print_view", - "tests/test_view.py::TestView::test_relations", - "tests/test_view.py::TestView::test_view_type" -] \ No newline at end of file From c535db5cf354e3a297077e5ec56060fc8f6c7972 Mon Sep 17 00:00:00 2001 From: Shyam Upadhyay Date: Tue, 5 Feb 2019 13:25:58 -0500 Subject: [PATCH 05/17] readme --- ccg_nlpy/server/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 ccg_nlpy/server/README.md diff --git a/ccg_nlpy/server/README.md b/ccg_nlpy/server/README.md new file mode 100644 index 0000000..3c34d89 --- /dev/null +++ b/ccg_nlpy/server/README.md @@ -0,0 +1,8 @@ +To make a demo using your fancy pytorch / tensorflow / dynet model, you need to + +1. Create a Annotator by subclassing the Annotator class in `annotator.py`. + - You need to implement the `add_view` method, that will internally call your model. + +2. Write a method similar to get_view_from_model in `example/example_model.py`. This method name could be anything, you are responsible for calling this in the `add_view` method above. + +3. Write a `server.py` similar to `example/example_server.py`, where you create your model, wrap it into the annotator class you wrote, and expose its annotate method using flask server. \ No newline at end of file From 955a98e655ff1726e87f056c681b7c95e2c68718 Mon Sep 17 00:00:00 2001 From: Shyam Upadhyay Date: Mon, 18 Feb 2019 09:37:08 -0500 Subject: [PATCH 06/17] better example README --- ccg_nlpy/server/README.md | 64 +++++++++++++++++++- ccg_nlpy/server/example/example_annotator.py | 3 +- ccg_nlpy/server/example/example_model.py | 7 ++- 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/ccg_nlpy/server/README.md b/ccg_nlpy/server/README.md index 3c34d89..d18b1cf 100644 --- a/ccg_nlpy/server/README.md +++ b/ccg_nlpy/server/README.md @@ -1,8 +1,66 @@ +This folder contains the neccessary code for serving your python model through a visualization tool like apelles which can consume text annotations. + To make a demo using your fancy pytorch / tensorflow / dynet model, you need to -1. Create a Annotator by subclassing the Annotator class in `annotator.py`. - - You need to implement the `add_view` method, that will internally call your model. +## Create Your Annotator +Create a Annotator by subclassing the Annotator class in `annotator.py`. +This class wraps around your model, and specifies what view will be provided by your model and what views are required. + +You need to implement the `add_view` method, that will internally call your model. +For example, the ExampleAnnotator in `example/example_annotator` implements a `add_view` method that calls the model to get a new view that is then added to the text annotation. + +```python + def add_view(self, docta): + # ask the model to create the new view + new_view = self.model.get_view_from_text_annotation(docta) + # add it to the text annotation + new_view.view_name = self.provided_view + docta.view_dictionary[self.provided_view] = new_view + return docta +``` + +You also need to implement a `get_text_annotation_for_model` method that creates a text annotation (by calling either a local or remote pipeline) that contains all the neccessary views needed by your model (for instance, Wikifier needs NER view). + +```python + def get_text_annotation_for_model(self, text: str, required_views: List[str]): + text = text.replace("\n", "") + pretokenized_text = [text.split(" ")] + required_views = ",".join(required_views) + ta_json = self.pipeline.call_server_pretokenized(pretokenized_text=pretokenized_text, views=required_views) + ta = TextAnnotation(json_str=ta_json) + return ta +``` 2. Write a method similar to get_view_from_model in `example/example_model.py`. This method name could be anything, you are responsible for calling this in the `add_view` method above. -3. Write a `server.py` similar to `example/example_server.py`, where you create your model, wrap it into the annotator class you wrote, and expose its annotate method using flask server. \ No newline at end of file +```python + def get_view_from_model(self, docta:TextAnnotation) -> View: + # This upcases each token. Test for TokenLabelView + new_view = copy.deepcopy(docta.get_view("TOKENS")) + tokens = docta.get_tokens + for token, cons in zip(tokens, new_view.cons_list): + cons["label"] = token.upper() + return new_view +``` + +## Write the Server +Write a `server.py` similar to `example/example_server.py`. +This is where you instantiate your model, wrap it into the annotator class you wrote, and expose its annotate method using flask server. + + +```python +mymodel = ExampleModel() +# this could have been a remote pipeline. +pipeline = local_pipeline.LocalPipeline() +# specify the view that your annotator will provide, and the views that it will require. +annotator = ExampleAnnotator(model=mymodel, pipeline=pipeline, provided_view="DUMMYVIEW", required_views=["TOKENS"]) + +# expose the annotate method using flask. +app.add_url_rule(rule='/annotate', endpoint='annotate', view_func=annotator.annotate, methods=['GET']) +app.run(host='localhost', port=5000) +``` + +This will host the server on [localhost](http://127.0.0.1:5000/) and you can get your text annotation in json format by +sending requests to the server like so, + +[localhost/annotate?text=Shyam is a person and Apple is an organization&views=DUMMYVIEW]() \ No newline at end of file diff --git a/ccg_nlpy/server/example/example_annotator.py b/ccg_nlpy/server/example/example_annotator.py index 976ee4d..089612d 100644 --- a/ccg_nlpy/server/example/example_annotator.py +++ b/ccg_nlpy/server/example/example_annotator.py @@ -22,8 +22,9 @@ def __init__(self, model, pipeline: PipelineBase, provided_view: str, required_v # raise NotImplementedError def add_view(self, docta): + # ask the model to create the new view new_view = self.model.get_view_from_text_annotation(docta) - + # add it to the text annotation new_view.view_name = self.provided_view docta.view_dictionary[self.provided_view] = new_view return docta diff --git a/ccg_nlpy/server/example/example_model.py b/ccg_nlpy/server/example/example_model.py index c2eeff7..c17f708 100644 --- a/ccg_nlpy/server/example/example_model.py +++ b/ccg_nlpy/server/example/example_model.py @@ -10,6 +10,12 @@ def __init__(self): pass def get_view_from_model(self, docta:TextAnnotation) -> View: + """ + This method is where your model will create the new view that will get added to the text annotation. + The input docta text annotation should already contain all the views that are needed by your model. + :param docta: + :return: + """ # This upcases each token. Test for TokenLabelView new_view = copy.deepcopy(docta.get_view("TOKENS")) tokens = docta.get_tokens @@ -20,5 +26,4 @@ def get_view_from_model(self, docta:TextAnnotation) -> View: # new_view = copy.deepcopy(docta.get_view("NER_CONLL")) # for nercons in new_view.cons_list: # nercons["label"] = nercons["tokens"].upper() - return new_view \ No newline at end of file From 976b54d855fc7d7417f450e29819b547b04e2dad Mon Sep 17 00:00:00 2001 From: Shyam Upadhyay Date: Mon, 18 Feb 2019 09:39:36 -0500 Subject: [PATCH 07/17] Update README.md --- ccg_nlpy/server/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ccg_nlpy/server/README.md b/ccg_nlpy/server/README.md index d18b1cf..bbec794 100644 --- a/ccg_nlpy/server/README.md +++ b/ccg_nlpy/server/README.md @@ -1,6 +1,6 @@ This folder contains the neccessary code for serving your python model through a visualization tool like apelles which can consume text annotations. -To make a demo using your fancy pytorch / tensorflow / dynet model, you need to +To make a demo using your fancy pytorch / tensorflow / dynet model, you need to write your annotator, write a method to create new views in your model, and write the server. ## Create Your Annotator Create a Annotator by subclassing the Annotator class in `annotator.py`. @@ -31,7 +31,9 @@ You also need to implement a `get_text_annotation_for_model` method that creates return ta ``` -2. Write a method similar to get_view_from_model in `example/example_model.py`. This method name could be anything, you are responsible for calling this in the `add_view` method above. +## Add Method to Create View in your Model + +Write a method similar to get_view_from_model in `example/example_model.py`. This method name could be anything, you are responsible for calling this in the `add_view` method above. ```python def get_view_from_model(self, docta:TextAnnotation) -> View: @@ -63,4 +65,4 @@ app.run(host='localhost', port=5000) This will host the server on [localhost](http://127.0.0.1:5000/) and you can get your text annotation in json format by sending requests to the server like so, -[localhost/annotate?text=Shyam is a person and Apple is an organization&views=DUMMYVIEW]() \ No newline at end of file +[localhost/annotate?text=Shyam is a person and Apple is an organization&views=DUMMYVIEW](localhost/annotate?text=Shyam is a person and Apple is an organization&views=DUMMYVIEW) From 3e6413d01209718358a20f6d26e4d7306a4b1900 Mon Sep 17 00:00:00 2001 From: Shyam Upadhyay Date: Mon, 18 Feb 2019 09:40:25 -0500 Subject: [PATCH 08/17] Update README.md --- ccg_nlpy/server/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ccg_nlpy/server/README.md b/ccg_nlpy/server/README.md index bbec794..6bcb4bf 100644 --- a/ccg_nlpy/server/README.md +++ b/ccg_nlpy/server/README.md @@ -65,4 +65,4 @@ app.run(host='localhost', port=5000) This will host the server on [localhost](http://127.0.0.1:5000/) and you can get your text annotation in json format by sending requests to the server like so, -[localhost/annotate?text=Shyam is a person and Apple is an organization&views=DUMMYVIEW](localhost/annotate?text=Shyam is a person and Apple is an organization&views=DUMMYVIEW) +(localhost/annotate?text=Shyam is a person and Apple is an organization&views=DUMMYVIEW)[localhost/annotate?text=Shyam is a person and Apple is an organization&views=DUMMYVIEW] From 7d885d90cdca701df89cf8e380e2c6dc4f753563 Mon Sep 17 00:00:00 2001 From: Shyam Upadhyay Date: Mon, 18 Feb 2019 09:44:07 -0500 Subject: [PATCH 09/17] Update README.md --- ccg_nlpy/server/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ccg_nlpy/server/README.md b/ccg_nlpy/server/README.md index 6bcb4bf..a34c5c3 100644 --- a/ccg_nlpy/server/README.md +++ b/ccg_nlpy/server/README.md @@ -1,6 +1,6 @@ This folder contains the neccessary code for serving your python model through a visualization tool like apelles which can consume text annotations. -To make a demo using your fancy pytorch / tensorflow / dynet model, you need to write your annotator, write a method to create new views in your model, and write the server. +To make a demo using your fancy pytorch / tensorflow / dynet model, you need to (a) write your annotator, (b) write a method to create new views in your model, and (c) write the server. ## Create Your Annotator Create a Annotator by subclassing the Annotator class in `annotator.py`. @@ -64,5 +64,6 @@ app.run(host='localhost', port=5000) This will host the server on [localhost](http://127.0.0.1:5000/) and you can get your text annotation in json format by sending requests to the server like so, - -(localhost/annotate?text=Shyam is a person and Apple is an organization&views=DUMMYVIEW)[localhost/annotate?text=Shyam is a person and Apple is an organization&views=DUMMYVIEW] +``` +http://localhost/annotate?text="Shyam is a person and Apple is an organization"&views=DUMMYVIEW +``` From 11eaf0b877677ec9794568adfbe6c07863040cae Mon Sep 17 00:00:00 2001 From: Shyam Upadhyay Date: Mon, 18 Feb 2019 09:47:13 -0500 Subject: [PATCH 10/17] Update README.md --- ccg_nlpy/server/README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ccg_nlpy/server/README.md b/ccg_nlpy/server/README.md index a34c5c3..2f6fe26 100644 --- a/ccg_nlpy/server/README.md +++ b/ccg_nlpy/server/README.md @@ -1,6 +1,9 @@ This folder contains the neccessary code for serving your python model through a visualization tool like apelles which can consume text annotations. -To make a demo using your fancy pytorch / tensorflow / dynet model, you need to (a) write your annotator, (b) write a method to create new views in your model, and (c) write the server. +To make a demo using your fancy pytorch / tensorflow / dynet model, you need to +- Write your annotator, +- Write a method to create new views in your model, and +- Write the server. ## Create Your Annotator Create a Annotator by subclassing the Annotator class in `annotator.py`. @@ -61,8 +64,7 @@ annotator = ExampleAnnotator(model=mymodel, pipeline=pipeline, provided_view="DU app.add_url_rule(rule='/annotate', endpoint='annotate', view_func=annotator.annotate, methods=['GET']) app.run(host='localhost', port=5000) ``` - -This will host the server on [localhost](http://127.0.0.1:5000/) and you can get your text annotation in json format by +Running server.py will host the server on [localhost](http://127.0.0.1:5000/) and you can get your text annotation in json format by sending requests to the server like so, ``` http://localhost/annotate?text="Shyam is a person and Apple is an organization"&views=DUMMYVIEW From a9cab9e3826c89eabb8f33719038c4cca3aeda07 Mon Sep 17 00:00:00 2001 From: Shyam Upadhyay Date: Mon, 18 Feb 2019 09:47:46 -0500 Subject: [PATCH 11/17] Update README.md --- ccg_nlpy/server/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ccg_nlpy/server/README.md b/ccg_nlpy/server/README.md index 2f6fe26..e4b46f3 100644 --- a/ccg_nlpy/server/README.md +++ b/ccg_nlpy/server/README.md @@ -10,7 +10,7 @@ Create a Annotator by subclassing the Annotator class in `annotator.py`. This class wraps around your model, and specifies what view will be provided by your model and what views are required. You need to implement the `add_view` method, that will internally call your model. -For example, the ExampleAnnotator in `example/example_annotator` implements a `add_view` method that calls the model to get a new view that is then added to the text annotation. +For example, the `ExampleAnnotator` in `example/example_annotator` implements a `add_view` method that calls the model to get a new view that is then added to the text annotation. ```python def add_view(self, docta): From a8d8f1797c005870c4a53e74dd908a650f12ec01 Mon Sep 17 00:00:00 2001 From: Shyam Upadhyay Date: Mon, 18 Feb 2019 09:57:05 -0500 Subject: [PATCH 12/17] more descriptions --- ccg_nlpy/server/annotator.py | 10 +++++++++- ccg_nlpy/server/multi_annotator.py | 12 ++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/ccg_nlpy/server/annotator.py b/ccg_nlpy/server/annotator.py index eaa3763..be5751d 100644 --- a/ccg_nlpy/server/annotator.py +++ b/ccg_nlpy/server/annotator.py @@ -7,7 +7,15 @@ class Annotator: - def __init__(self, pipeline: PipelineBase, provided_view:str, required_views:List[str]): + """ + Wraps around your python model, and calls it to get new views that are provided by it. + The annotate method implemented below is exposed through the flask server. + + You should subclass this class and implement add_view and get_text_annotation_for_model methods. + If you are serving a multilingual model, please see multi_annotator.py also. + """ + + def __init__(self, pipeline: PipelineBase, provided_view: str, required_views: List[str]): # the viewname provided by the model self.provided_view = provided_view # the views required by the model (e.g. NER_CONLL for Wikifier) diff --git a/ccg_nlpy/server/multi_annotator.py b/ccg_nlpy/server/multi_annotator.py index c41806c..0811744 100644 --- a/ccg_nlpy/server/multi_annotator.py +++ b/ccg_nlpy/server/multi_annotator.py @@ -9,6 +9,18 @@ class MultiAnnotator: + """ + Wraps around several Annotator instances for serving multiple models simultaneously from a single endpoint. + The intended use case is for serving multilingual models (e.g., NER for English, Spanish, Chinese ...) + + All the annotators should have the same set of required views. + + The annotate method implemented below is exposed through the flask server. + It identifies the relevant annotator to call. + For instance, if the NER_zh view is requested, it will call the annotator that provides it. + + You should subclass this class and implement add_view method. + """ def __init__(self, annotators: List[Annotator]): self.annotators = annotators # all models should have the same set of required views From 3968eca40be32d6b07bedb5b2fd6a2a8c58225f8 Mon Sep 17 00:00:00 2001 From: Shyam Upadhyay Date: Mon, 18 Feb 2019 09:57:39 -0500 Subject: [PATCH 13/17] Update README.md --- ccg_nlpy/server/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ccg_nlpy/server/README.md b/ccg_nlpy/server/README.md index e4b46f3..cdd37c0 100644 --- a/ccg_nlpy/server/README.md +++ b/ccg_nlpy/server/README.md @@ -1,10 +1,12 @@ -This folder contains the neccessary code for serving your python model through a visualization tool like apelles which can consume text annotations. +This folder contains the neccessary code for serving your python model through a visualization tool like [apelles](https://github.com/CogComp/apelles) which can consume text annotation jsons. To make a demo using your fancy pytorch / tensorflow / dynet model, you need to - Write your annotator, - Write a method to create new views in your model, and - Write the server. +If you are trying to serve a multilingual model, you should also look at `multi_annotator.py`. + ## Create Your Annotator Create a Annotator by subclassing the Annotator class in `annotator.py`. This class wraps around your model, and specifies what view will be provided by your model and what views are required. From 81a2f3a603bbcb46c74bfae652f58fc8790c0236 Mon Sep 17 00:00:00 2001 From: Shyam Upadhyay Date: Mon, 18 Feb 2019 10:02:24 -0500 Subject: [PATCH 14/17] more description --- ccg_nlpy/server/annotator.py | 9 +++++++++ ccg_nlpy/server/multi_annotator.py | 8 ++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/ccg_nlpy/server/annotator.py b/ccg_nlpy/server/annotator.py index be5751d..42df96f 100644 --- a/ccg_nlpy/server/annotator.py +++ b/ccg_nlpy/server/annotator.py @@ -16,6 +16,15 @@ class Annotator: """ def __init__(self, pipeline: PipelineBase, provided_view: str, required_views: List[str]): + """ + The required arguments are + (a) a pipeline instance (either local or remote) that will be used to create text annotations that will be sent to the model. + (b) the name of the view provided by the model + (c) the list of view names required by the model (e.g., NER for Wikifier). + :param pipeline: pipeline instance (local or remote) + :param provided_view: view name provided + :param required_views: list of view names required + """ # the viewname provided by the model self.provided_view = provided_view # the views required by the model (e.g. NER_CONLL for Wikifier) diff --git a/ccg_nlpy/server/multi_annotator.py b/ccg_nlpy/server/multi_annotator.py index 0811744..8379b5d 100644 --- a/ccg_nlpy/server/multi_annotator.py +++ b/ccg_nlpy/server/multi_annotator.py @@ -19,9 +19,13 @@ class MultiAnnotator: It identifies the relevant annotator to call. For instance, if the NER_zh view is requested, it will call the annotator that provides it. - You should subclass this class and implement add_view method. - """ + You should subclass this class and implement the add_view method. + """ def __init__(self, annotators: List[Annotator]): + """ + Takes a list of annotator instances that provide views for different languages + :param annotators: list of annotator instances + """ self.annotators = annotators # all models should have the same set of required views self.required_views = annotators[0].get_required_views() From 781580367d923d0a61aa1212b1140de8220eadfb Mon Sep 17 00:00:00 2001 From: Shyam Upadhyay Date: Mon, 18 Feb 2019 10:07:27 -0500 Subject: [PATCH 15/17] Update README.md --- ccg_nlpy/server/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ccg_nlpy/server/README.md b/ccg_nlpy/server/README.md index cdd37c0..09d0f30 100644 --- a/ccg_nlpy/server/README.md +++ b/ccg_nlpy/server/README.md @@ -1,9 +1,9 @@ This folder contains the neccessary code for serving your python model through a visualization tool like [apelles](https://github.com/CogComp/apelles) which can consume text annotation jsons. To make a demo using your fancy pytorch / tensorflow / dynet model, you need to -- Write your annotator, -- Write a method to create new views in your model, and -- Write the server. +- [Write your annotator](#create-your-annotator), +- [Write a method to create new views in your model](#add-method-to-create-view-in-your-model), and +- [Write the server](#write-the-server). If you are trying to serve a multilingual model, you should also look at `multi_annotator.py`. From 2845fe10255acd57d5677c793b2f0f2580d3d62c Mon Sep 17 00:00:00 2001 From: Shyam Upadhyay Date: Mon, 18 Feb 2019 10:15:28 -0500 Subject: [PATCH 16/17] minor --- ccg_nlpy/server/README.md | 22 +++++++++++++++++++++- ccg_nlpy/server/multi_annotator.py | 8 -------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/ccg_nlpy/server/README.md b/ccg_nlpy/server/README.md index 09d0f30..2807617 100644 --- a/ccg_nlpy/server/README.md +++ b/ccg_nlpy/server/README.md @@ -5,7 +5,7 @@ To make a demo using your fancy pytorch / tensorflow / dynet model, you need to - [Write a method to create new views in your model](#add-method-to-create-view-in-your-model), and - [Write the server](#write-the-server). -If you are trying to serve a multilingual model, you should also look at `multi_annotator.py`. +If you are trying to serve a multilingual model, you should also look at [serving multiple models](#serving-multiple-models). ## Create Your Annotator Create a Annotator by subclassing the Annotator class in `annotator.py`. @@ -71,3 +71,23 @@ sending requests to the server like so, ``` http://localhost/annotate?text="Shyam is a person and Apple is an organization"&views=DUMMYVIEW ``` + +## Serving Multiple Models + +For serving multiple models using a single server, as in the case of multilingual models, there is a utility class `multi_annotator.py` that wraps around several annotator instances. +For instance, you can serve NER_English, NER_Spanish, etc. all through a single server using the `MultiAnnotator` class in `multi_annotator.py`. +You can use it to write your `server.py` that provides multiple views as follows, + +```python + annotators: List[Annotator] = [] + langs = ["es", "zh", "fr", "it", "de"] + model_paths = [...] + for lang, model_path in zip(langs, model_paths): + annotator = ... # Create your language specific annotators here + annotators.append(annotator) + + multi_annotator = MultiAnnotator(annotators=annotators) + app.add_url_rule(rule='/annotate', endpoint='annotate', view_func=multi_annotator.annotate, methods=['GET']) + app.run(host='0.0.0.0', port=8009) + +``` \ No newline at end of file diff --git a/ccg_nlpy/server/multi_annotator.py b/ccg_nlpy/server/multi_annotator.py index 8379b5d..74c4686 100644 --- a/ccg_nlpy/server/multi_annotator.py +++ b/ccg_nlpy/server/multi_annotator.py @@ -19,7 +19,6 @@ class MultiAnnotator: It identifies the relevant annotator to call. For instance, if the NER_zh view is requested, it will call the annotator that provides it. - You should subclass this class and implement the add_view method. """ def __init__(self, annotators: List[Annotator]): """ @@ -55,13 +54,6 @@ def get_view_names(self) -> List[str]: """ return self.provided_views - def add_view(self, docta: TextAnnotation) -> TextAnnotation: - """ - Takes a text annotation and adds the view provided by this model to it. - :return: TextAnnotation - """ - raise NotImplementedError - def annotate(self): # we get something like "?text=&views=". Below two lines extract these. text = request.args.get('text') From 524b3416fd72a795ed302646c79fb93ccb5d764e Mon Sep 17 00:00:00 2001 From: Shyam Upadhyay Date: Mon, 18 Feb 2019 10:16:48 -0500 Subject: [PATCH 17/17] Update README.md --- ccg_nlpy/server/README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ccg_nlpy/server/README.md b/ccg_nlpy/server/README.md index 2807617..dd43aea 100644 --- a/ccg_nlpy/server/README.md +++ b/ccg_nlpy/server/README.md @@ -76,6 +76,8 @@ http://localhost/annotate?text="Shyam is a person and Apple is an organization"& For serving multiple models using a single server, as in the case of multilingual models, there is a utility class `multi_annotator.py` that wraps around several annotator instances. For instance, you can serve NER_English, NER_Spanish, etc. all through a single server using the `MultiAnnotator` class in `multi_annotator.py`. +Make sure that the annotators that are served using a single `MultiAnnotator` all have the same required view. + You can use it to write your `server.py` that provides multiple views as follows, ```python @@ -88,6 +90,6 @@ You can use it to write your `server.py` that provides multiple views as follows multi_annotator = MultiAnnotator(annotators=annotators) app.add_url_rule(rule='/annotate', endpoint='annotate', view_func=multi_annotator.annotate, methods=['GET']) - app.run(host='0.0.0.0', port=8009) + app.run(host='localhost', port=5000) -``` \ No newline at end of file +```