Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: restructure Model API #86

Merged
merged 5 commits into from
May 24, 2023
Merged
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
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