Skip to content

Commit

Permalink
feat: make Model API interface more explicit (DS4SD#86)
Browse files Browse the repository at this point in the history
Signed-off-by: Panos Vagenas <35837085+vagenas@users.noreply.github.com>
  • Loading branch information
vagenas authored and miguel-brandao-ibm committed Jun 1, 2023
1 parent 3effcfc commit 2aefb2c
Show file tree
Hide file tree
Showing 12 changed files with 252 additions and 217 deletions.
2 changes: 1 addition & 1 deletion .github/actions/setup-poetry/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ runs:
python-version: ${{ inputs.python-version }}
cache: 'poetry'
- name: Install dependencies
run: poetry install
run: poetry install --all-extras
shell: bash
57 changes: 50 additions & 7 deletions deepsearch/model/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,64 @@
The Model API allows users to serve and integrate their own models.

## Installation
To use the Model API, install including the `api`
extra, i.e.:
- with poetry:
`poetry add "deepsearch-toolkit[api]"`
- with pip: `pip install "deepsearch-toolkit[api]"`

## Usage
To run a model, register it with a
[`DeepSearchAnnotatorApp`](server/deepsearch_annotator_app.py) and run the app:
```python
from deepsearch.model.server.deepsearch_annotator_app import DeepSearchAnnotatorApp

annotator = ... # e.g. SimpleTextGeographyAnnotator()
app = DeepSearchAnnotatorApp()
app.register_annotator(annotator)
app.run()
app.run(host="127.0.0.1", port=8000)
```

For a complete example, check [examples/main.py](examples/main.py).
### OpenAPI

The OpenAPI UI is served under `/docs`, e.g. http://127.0.0.1:8000/docs.

#### Inference

An example input payload for the `/predict` endpoint would look as follows
(note that `deepsearch.res.ibm.com/x-deadline` should be a future timestamp):
```json
{
"apiVersion": "v1",
"kind": "NLPModel",
"metadata": {
"annotations": {
"deepsearch.res.ibm.com/x-deadline": "2024-04-20T12:26:01.479484+00:00",
"deepsearch.res.ibm.com/x-transaction-id": "abc",
"deepsearch.res.ibm.com/x-attempt-number": 5,
"deepsearch.res.ibm.com/x-max-attempts": 5
}
},
"spec": {
"findEntities": {
"entityNames": ["cities", "countries"],
"objectType": "text",
"texts": [
"Bern, the capital city of Switzerland, is built around a crook in the Aare River.",
"Athens is a major coastal urban area in the Mediterranean and is both the capital and largest city of Greece."
]
}
}
}
```

## Models
For an example, check
[examples/simple_text_geography_annotator/](examples/simple_text_geography_annotator/).
## Developing a new model
To develop a new model class, inherit from a [base model class](base/) and implement the
methods and attributes that are declared as abstract.

## Base models
Check [base/](base/).
### Examples
- Minimal dummy annotator:
[examples/minimal_annotator/](examples/minimal_annotator)
- Simple geography annotator:
[examples/simple_text_geography_annotator/](examples/simple_text_geography_annotator/)
189 changes: 21 additions & 168 deletions deepsearch/model/base/base_annotator.py
Original file line number Diff line number Diff line change
@@ -1,172 +1,34 @@
# Interface -> defines mandatory functions for annotators
from abc import ABCMeta, abstractproperty
from typing import List, Optional, Union
from abc import ABC, abstractmethod
from typing import List

from fastapi import HTTPException
from pydantic import BaseModel, ValidationError

class BaseAnnotator(ABC):

class BaseAnnotator_properties(ABCMeta):
version: str = "undefined"
url: str = "undefined"
author: str = "undefined"
description: str = "undefined"
expected_compute_time: float = 1.0
labels: dict = {}

supports = (
entities
) = (
relationships
) = (
properties
) = (
name
) = version = url = author = description = expected_compute_time = abstractproperty
@property
@abstractmethod
def kind(self) -> str:
return self.kind

def __call__(self, *args, **kwargs):
obj = super(BaseAnnotator_properties, self).__call__(*args, **kwargs)
try:
getattr(obj, "supports")
except AttributeError:
self.supports = []
try:
getattr(obj, "entities")
except AttributeError:
self.entities = []
try:
getattr(obj, "relationships")
except AttributeError:
self.relationships = []
try:
getattr(obj, "properties")
except AttributeError:
self.properties = []
try:
getattr(obj, "name")
except AttributeError:
self.name = "undefined"
try:
getattr(obj, "version")
except AttributeError:
self.version = "undefined"
try:
getattr(obj, "url")
except AttributeError:
self.url = "undefined"
try:
getattr(obj, "author")
except AttributeError:
self.author = "undefined"
try:
getattr(obj, "description")
except AttributeError:
self.description = "undefined"
try:
getattr(obj, "expected_compute_time")
except AttributeError:
self.expected_compute_time = 1.0
try:
getattr(obj, "kind")
except AttributeError:
self.kind = "undefined"
@property
@abstractmethod
def name(self) -> str:
return self.name

return obj


class BaseAnnotator(metaclass=BaseAnnotator_properties):

kind: str
supports: Union[tuple, list]
name: str
version: str
url: str
author: str
description: str
expected_compute_time: float
labels: dict

def annotate_batched_entities(
self, object_type: str, items: List, entity_names: Optional[List[str]]
) -> List[dict]:
results = []
for item in items:
try:
results.append(
self.annotate_entities(object_type, item, entity_names)[0]
)
except HTTPException as e:
raise e
return results

def annotate_batched_properties(
self,
object_type: str,
items: List,
entities: List[dict],
property_names: Optional[List[str]],
) -> List[dict]:
results = []
for item, entity in zip(items, entities):
try:
results.append(
self.annotate_properties(object_type, item, entity, property_names)[
0
]
)
except HTTPException as e:
raise e
return results

def annotate_batched_relationships(
self,
object_type: str,
items: List,
entities: List[dict],
relationship_names: Optional[List[str]],
) -> List[dict]:
results = []
for item, entity in zip(items, entities):
try:
results.append(
self.annotate_relationships(
object_type, item, entity, relationship_names
)[0]
)
except HTTPException as e:
raise e
return results

def annotate_entities(
self, object_type: str, item: List, entity_names: Optional[List[str]]
) -> List[dict]:
raise HTTPException(
status_code=501, detail="Unsuported Operation for annotator: findEntities"
)

def annotate_properties(
self,
object_type: str,
item: str,
entity: dict,
property_names: Optional[List[str]],
) -> List[dict]:
# Incomplete
raise HTTPException(
status_code=501, detail="Unsuported Operation for annotator: findProperties"
)

def annotate_relationships(
self,
object_type: str,
item: str,
entity: dict,
relationship_names: Optional[List[str]],
) -> List[dict]:
# Incomplete
raise HTTPException(
status_code=501,
detail="Unsuported Operation for annotator: findRelationships",
)
@property
@abstractmethod
def supports(self) -> List[str]:
return self.supports

def get_annotator_info(self) -> dict:
annotator_info = {
"definitions": {
# The extensive url in the issue proposition
"apiVersion": "v1",
"kind": self.kind,
"spec": {
Expand All @@ -185,12 +47,3 @@ def get_annotator_info(self) -> dict:
}

return annotator_info

# def get_entity_names(self):
# return self.annotator_info["spec"]["definition"]["entities"]
#
# def get_relationship_names(self):
# return self.annotator_info["spec"]["definition"]["entities"]
#
# def get_property_names(self):
# return self.annotator_info["spec"]["definition"]["entities"]
57 changes: 57 additions & 0 deletions deepsearch/model/base/base_nlp_annotator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from abc import abstractmethod
from typing import List, Optional

from deepsearch.model.base.base_annotator import BaseAnnotator


class BaseNLPAnnotator(BaseAnnotator):

kind: str = "NLPModel"

@abstractmethod
def annotate_batched_entities(
self,
object_type: str,
items: List[str],
entity_names: Optional[List[str]],
) -> List[dict]:
"""Annotate entities in given items batch.
Args:
object_type (str): type of objects to annotate, e.g. "text"; must be included
in `supports` property
items (List[str]): batch of input items to annotate
entity_names (Optional[List[str]]): entities to annotate
Returns:
List[dict]: a list, which, for each item in `items`, contains a dict with keys
being the various entity names each mapped to a list of its annotations
(can be empty) in the item, each annotation being a dict like:
{
"type": <entity_name>,
"match": <match>,
"original": <original>,
"range": [<range_start>, <range_end>]
}
"""
return []

@abstractmethod
def annotate_batched_relationships(
self,
object_type: str,
items: List[str],
entities: List[dict],
relationship_names: Optional[List[str]],
) -> List[dict]:
return []

@abstractmethod
def annotate_batched_properties(
self,
object_type: str,
items: List[str],
entities: List[dict],
property_names: Optional[List[str]],
) -> List[dict]:
return []
Empty file.
14 changes: 14 additions & 0 deletions deepsearch/model/examples/minimal_annotator/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from deepsearch.model.examples.minimal_annotator.minimal_annotator import (
MinimalAnnotator,
)
from deepsearch.model.server.deepsearch_annotator_app import DeepSearchAnnotatorApp


def run():
app = DeepSearchAnnotatorApp()
app.register_annotator(MinimalAnnotator())
app.run()


if __name__ == "__main__":
run()
Loading

0 comments on commit 2aefb2c

Please sign in to comment.