Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ html/

# Python egg metadata, regenerated from source files by setuptools.
/*.egg-info

.cache
.pytest_cache
95 changes: 95 additions & 0 deletions ccg_nlpy/server/README.md
Original file line number Diff line number Diff line change
@@ -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)

```
90 changes: 78 additions & 12 deletions ccg_nlpy/server/annotator.py
Original file line number Diff line number Diff line change
@@ -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:
"""
Expand All @@ -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
# we get something like "?text=<text>&views=<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
41 changes: 0 additions & 41 deletions ccg_nlpy/server/example/dummy_annotator.py

This file was deleted.

39 changes: 39 additions & 0 deletions ccg_nlpy/server/example/example_annotator.py
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions ccg_nlpy/server/example/example_model.py
Original file line number Diff line number Diff line change
@@ -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
30 changes: 0 additions & 30 deletions ccg_nlpy/server/example/example_model_wrapper_server.py

This file was deleted.

26 changes: 26 additions & 0 deletions ccg_nlpy/server/example/example_server.py
Original file line number Diff line number Diff line change
@@ -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()
Loading