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/ccg_nlpy/server/README.md b/ccg_nlpy/server/README.md new file mode 100644 index 0000000..dd43aea --- /dev/null +++ b/ccg_nlpy/server/README.md @@ -0,0 +1,95 @@ +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](#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 [serving multiple models](#serving-multiple-models). + +## 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 +``` + +## 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: + # 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) +``` +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 +``` + +## 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`. +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 + 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='localhost', port=5000) + +``` diff --git a/ccg_nlpy/server/annotator.py b/ccg_nlpy/server/annotator.py index 6cc03d1..42df96f 100644 --- a/ccg_nlpy/server/annotator.py +++ b/ccg_nlpy/server/annotator.py @@ -1,22 +1,56 @@ 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: + """ + 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]): """ - Load the relevant model parameters. - :return: None + 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 """ - raise NotImplementedError + # 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 = pipeline + 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: """ - 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 +59,41 @@ 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_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..089612d --- /dev/null +++ b/ccg_nlpy/server/example/example_annotator.py @@ -0,0 +1,39 @@ +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, 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): + # logging.info("loading model params ...") + # 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 + + 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..c17f708 --- /dev/null +++ b/ccg_nlpy/server/example/example_model.py @@ -0,0 +1,29 @@ +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 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 + 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..05b3aa5 --- /dev/null +++ b/ccg_nlpy/server/example/example_server.py @@ -0,0 +1,26 @@ +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 + +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. + 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']) + 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/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_annotator.py b/ccg_nlpy/server/multi_annotator.py new file mode 100644 index 0000000..74c4686 --- /dev/null +++ b/ccg_nlpy/server/multi_annotator.py @@ -0,0 +1,81 @@ +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: + """ + 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. + + """ + 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() + 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 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 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: Annotator = self.view2annotator_dict[view] + # create a text ann with the required views for the model + 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 + 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" 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