diff --git a/docs/api.rst b/docs/api.rst index 33aab2c..8dedebd 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -62,6 +62,7 @@ NIF API :query o outformat: one of `turtle` (default), `text`, `json-ld` :query p prefix: prefix for the URIs :query algo algorithm: algorithm/plugin to use for the analysis. For a list of options, see :http:get:`/api/plugins`. If not provided, the default plugin will be used (:http:get:`/api/plugins/default`). + :query algo emotionModel: desired emotion model in the results. If the requested algorithm does not use that emotion model, there are conversion plugins specifically for this. If none of the plugins match, an error will be returned, which includes the results *as is*. :reqheader Accept: the response content type depends on :mailheader:`Accept` header @@ -69,6 +70,7 @@ NIF API header of request :statuscode 200: no error :statuscode 404: service not found + :statuscode 400: error while processing the request .. http:post:: /api @@ -93,51 +95,53 @@ NIF API { "@context": { ... - }, - "sentiment140": { - "name": "sentiment140", - "is_activated": true, - "version": "0.1", - "extra_params": { - "@id": "extra_params_sentiment140_0.1", - "language": { - "required": false, - "@id": "lang_sentiment140", - "options": [ - "es", - "en", - "auto" - ], - "aliases": [ - "language", - "l" - ] - } - }, - "@id": "sentiment140_0.1" - }, - "rand": { - "name": "rand", - "is_activated": true, - "version": "0.1", - "extra_params": { - "@id": "extra_params_rand_0.1", - "language": { - "required": false, - "@id": "lang_rand", - "options": [ - "es", - "en", - "auto" - ], - "aliases": [ - "language", - "l" - ] - } - }, - "@id": "rand_0.1" - } + }, + "@type": "plugins", + "plugins": [ + { + "name": "sentiment140", + "is_activated": true, + "version": "0.1", + "extra_params": { + "@id": "extra_params_sentiment140_0.1", + "language": { + "required": false, + "@id": "lang_sentiment140", + "options": [ + "es", + "en", + "auto" + ], + "aliases": [ + "language", + "l" + ] + } + }, + "@id": "sentiment140_0.1" + }, { + "name": "rand", + "is_activated": true, + "version": "0.1", + "extra_params": { + "@id": "extra_params_rand_0.1", + "language": { + "required": false, + "@id": "lang_rand", + "options": [ + "es", + "en", + "auto" + ], + "aliases": [ + "language", + "l" + ] + } + }, + "@id": "rand_0.1" + } + ] } @@ -148,7 +152,7 @@ NIF API .. sourcecode:: http - GET /api/plugins/rand HTTP/1.1 + GET /api/plugins/rand/ HTTP/1.1 Host: localhost Accept: application/json, text/javascript @@ -159,6 +163,7 @@ NIF API { "@id": "rand_0.1", + "@type": "sentimentPlugin", "extra_params": { "@id": "extra_params_rand_0.1", "language": { @@ -185,24 +190,3 @@ NIF API Return the information about the default plugin. -.. http:get:: /api/plugins//{de}activate - - {De}activate a plugin. - - **Example request**: - - .. sourcecode:: http - - GET /api/plugins/rand/deactivate HTTP/1.1 - Host: localhost - Accept: application/json, text/javascript - - - **Example response**: - - .. sourcecode:: http - - { - "@context": {}, - "message": "Ok" - } diff --git a/docs/conversion.rst b/docs/conversion.rst new file mode 100644 index 0000000..17791eb --- /dev/null +++ b/docs/conversion.rst @@ -0,0 +1,116 @@ +Introduction +------------ + +Senpy includes experimental support for emotion/sentiment conversion plugins. + + +Use +--- + +Consider the original query: `http://127.0.0.1:5000/api/?i=hello&algo=emoRand`_ + +The requested plugin (emoRand) returns emotions using Ekman's model (or big6 in EmotionML): + +.. code:: json + + + ... rest of the document ... + { + "@type": "emotionSet", + "onyx:hasEmotion": { + "@type": "emotion", + "onyx:hasEmotionCategory": "emoml:big6anger" + }, + "prov:wasGeneratedBy": "plugins/emoRand_0.1" + } + + + +To get these emotions in VAD space (FSRE dimensions in EmotionML), we'd do this: + +`http://127.0.0.1:5000/api/?i=hello&algo=emoRand&emotionModel=emoml:fsre-dimensions`_ + +This call, provided there is a valid conversion plugin from Ekman's to VAD, would return something like this: + +.. code:: json + + + ... rest of the document ... + { + "@type": "emotionSet", + "onyx:hasEmotion": { + "@type": "emotion", + "onyx:hasEmotionCategory": "emoml:big6anger" + }, + "prov:wasGeneratedBy": "plugins/emoRand_0.1" + }, { + "@type": "emotionSet", + "onyx:hasEmotion": { + "@type": "emotion", + "A": 7.22, + "D": 6.28, + "V": 8.6 + }, + "prov:wasGeneratedBy": "plugins/Ekman2VAD_0.1" + + } + + +That is called a *full* response, as it simply adds the converted emotion alongside. +It is also possible to get the original emotion nested within the new converted emotion, using the `conversion=nested` parameter: + +.. code:: json + + + ... rest of the document ... + { + "@type": "emotionSet", + "onyx:hasEmotion": { + "@type": "emotion", + "onyx:hasEmotionCategory": "emoml:big6anger" + }, + "prov:wasGeneratedBy": "plugins/emoRand_0.1" + "onyx:wasDerivedFrom": { + "@type": "emotionSet", + "onyx:hasEmotion": { + "@type": "emotion", + "A": 7.22, + "D": 6.28, + "V": 8.6 + }, + "prov:wasGeneratedBy": "plugins/Ekman2VAD_0.1" + } + + } + + +Lastly, `conversion=filtered` would only return the converted emotions. + +Developing a conversion plugin +------------------------------ + +Conversion plugins are discovered by the server just like any other plugin. +The difference is the slightly different API, and the need to specify the `source` and `target` of the conversion. +For instance, an emotion conversion plugin needs the following: + + +.. code:: yaml + + + --- + onyx:doesConversion: + - onyx:conversionFrom: emoml:big6 + onyx:conversionTo: emoml:fsre-dimensions + - onyx:conversionFrom: emoml:fsre-dimensions + onyx:conversionTo: emoml:big6 + + + + +.. code:: python + + + class MyConversion(EmotionConversionPlugin): + + def convert(self, emotionSet, fromModel, toModel, params): + pass diff --git a/docs/plugins.rst b/docs/plugins.rst index 91308f5..3c12242 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -1,5 +1,7 @@ Developing new plugins ---------------------- +This document describes how to develop a new analysis plugin. For an example of conversion plugins, see :doc:`conversion`. + Each plugin represents a different analysis process.There are two types of files that are needed by senpy for loading a plugin: - Definition file, has the ".senpy" extension. diff --git a/docs/usage.rst b/docs/usage.rst index 3d510c2..62876aa 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -48,8 +48,8 @@ Once the server is launched, there is a basic endpoint in the server, which prov In case you want to know the different endpoints of the server, there is more information available in the NIF API section_. -Video example -============= +CLI +=== This video shows how to use senpy through command-line tool. @@ -58,16 +58,17 @@ https://asciinema.org/a/9uwef1ghkjk062cw2t4mhzpyk Request example in python ========================= -This example shows how to make a request to a plugin. +This example shows how to make a request to the default plugin: .. code:: python - import requests - import json + from senpy.client import Client - r = requests.get('http://127.0.0.1:5000/api/?algo=rand&i=Testing') - response = r.content.decode('utf-8') - response_json = json.loads(response) + c = Client('http://127.0.0.1:5000/api/') + r = c.analyse('hello world') + + for entry in r.entries: + print('{} -> {}'.format(entry.text, entry.emotions)) diff --git a/senpy/client.py b/senpy/client.py index e64af35..e2810f2 100644 --- a/senpy/client.py +++ b/senpy/client.py @@ -18,7 +18,6 @@ class Client(object): try: resp = models.from_dict(response.json()) resp.validate(resp) - return resp except Exception as ex: logger.error(('There seems to be a problem with the response:\n' '\tURL: {url}\n' @@ -33,3 +32,6 @@ class Client(object): code=response.status_code, content=response.content)) raise ex + if isinstance(resp, models.Error): + raise resp + return resp diff --git a/tests/test_client.py b/tests/test_client.py index 2597258..c94af96 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -34,8 +34,11 @@ class ModelsTest(TestCase): url=endpoint + '/', method='GET', params={'input': 'hello'}) error = Call(Error('Nothing')) with patch('requests.request', return_value=error) as patched: - resp = client.analyse(input='hello', algorithm='NONEXISTENT') - assert isinstance(resp, Error) + try: + client.analyse(input='hello', algorithm='NONEXISTENT') + raise Exception('Exceptions should be raised. This is not golang') + except Error: + pass patched.assert_called_with( url=endpoint + '/', method='GET',