mirror of
https://github.com/gsi-upm/senpy
synced 2024-12-21 12:38:13 +00:00
Multiple changes in the API, schemas and UI
Check out the CHANGELOG.md file for more information
This commit is contained in:
parent
4ba30304a4
commit
8a516d927e
@ -28,6 +28,7 @@ test-3.5:
|
||||
|
||||
test-2.7:
|
||||
<<: *test_definition
|
||||
allow_failure: true
|
||||
variables:
|
||||
PYTHON_VERSION: "2.7"
|
||||
|
||||
|
@ -29,7 +29,7 @@ build: $(addprefix build-, $(PYVERSIONS)) ## Build all images / python versions
|
||||
docker tag $(IMAGEWTAG)-python$(PYMAIN) $(IMAGEWTAG)
|
||||
|
||||
build-%: version Dockerfile-% ## Build a specific version (e.g. build-2.7)
|
||||
docker build -t '$(IMAGEWTAG)-python$*' -f Dockerfile-$* .;
|
||||
docker build --pull -t '$(IMAGEWTAG)-python$*' -f Dockerfile-$* .;
|
||||
|
||||
dev-%: ## Launch a specific development environment using docker (e.g. dev-2.7)
|
||||
@docker start $(NAME)-dev$* || (\
|
||||
|
53
CHANGELOG.md
Normal file
53
CHANGELOG.md
Normal file
@ -0,0 +1,53 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
* Objects can control the keys that will be used in `serialize`/`jsonld`/`as_dict` by specifying a list of keys in `terse_keys`.
|
||||
e.g.
|
||||
```python
|
||||
>>> class MyModel(senpy.models.BaseModel):
|
||||
... _terse_keys = ['visible']
|
||||
... invisible = 5
|
||||
... visible = 1
|
||||
...
|
||||
>>> m = MyModel(id='testing')
|
||||
>>> m.jsonld()
|
||||
{'invisible': 5, 'visible': 1, '@id': 'testing'}
|
||||
>>> m.jsonld(verbose=False)
|
||||
{'visible': 1}
|
||||
```
|
||||
* Configurable logging format.
|
||||
* Added default terse keys for the most common classes (entry, sentiment, emotion...).
|
||||
* Flag parameters (boolean) are set to true even when no value is added (e.g. `&verbose` is the same as `&verbose=true`).
|
||||
* Plugin and parameter descriptions are now formatted with (showdown)[https://github.com/showdownjs/showdown].
|
||||
* The web UI requests extra_parameters from the server. This is useful for pipelines. See #52
|
||||
* First batch of semantic tests (using SPARQL)
|
||||
|
||||
### Changed
|
||||
* `install_deps` now checks what requirements are already met before installing with pip.
|
||||
* Help is now provided verbosely by default
|
||||
* Other outputs are terse by default. This means some properties are now hidden unless verbose is set.
|
||||
* `sentiments` and `emotions` are now `marl:hasOpinion` and `onyx:hasEmotionSet`, respectively.
|
||||
* Nicer logging format
|
||||
* Context aliases (e.g. `sentiments` and `emotions` properties) have been replaced with the original properties (e.g. `marl:hasOpinion` and `onyx:hasEmotionSet**), to use aliases, pass the `aliases** parameter.
|
||||
* Several UI improvements
|
||||
* Dedicated tab to show the list of plugins
|
||||
* URLs in plugin descriptions are shown as links
|
||||
* The format of the response is selected by clicking on a tab instead of selecting from a drop-down
|
||||
* list of examples
|
||||
* Bootstrap v4
|
||||
* RandEmotion and RandSentiment are no longer included in the base set of plugins
|
||||
* The `--plugin-folder` option can be used more than once, and every folder will be added to the app.
|
||||
|
||||
### Deprecated
|
||||
### Removed
|
||||
* Python 2.7 is no longer test or officially supported
|
||||
### Fixed
|
||||
* Plugin descriptions are now dedented when they are extracted from the docstring.
|
||||
### Security
|
||||
|
2
Makefile
2
Makefile
@ -5,7 +5,7 @@ IMAGENAME=gsiupm/senpy
|
||||
|
||||
# The first version is the main one (used for quick builds)
|
||||
# See .makefiles/python.mk for more info
|
||||
PYVERSIONS=3.5 2.7
|
||||
PYVERSIONS=3.6 3.7
|
||||
|
||||
DEVPORT=5000
|
||||
|
||||
|
2
Procfile
2
Procfile
@ -1 +1 @@
|
||||
web: python -m senpy --host 0.0.0.0 --port $PORT --default-plugins
|
||||
web: python -m senpy --host 0.0.0.0 --port $PORT
|
||||
|
27
README.rst
27
README.rst
@ -1,10 +1,19 @@
|
||||
.. image:: img/header.png
|
||||
:width: 100%
|
||||
:target: http://demos.gsi.dit.upm.es/senpy
|
||||
:target: http://senpy.gsi.upm.es
|
||||
|
||||
.. image:: https://travis-ci.org/gsi-upm/senpy.svg?branch=master
|
||||
:target: https://travis-ci.org/gsi-upm/senpy
|
||||
|
||||
.. image:: https://lab.gsi.upm.es/senpy/senpy/badges/master/pipeline.svg
|
||||
:target: https://lab.gsi.upm.es/senpy/senpy/commits/master
|
||||
|
||||
.. image:: https://lab.gsi.upm.es/senpy/senpy/badges/master/coverage.svg
|
||||
:target: https://lab.gsi.upm.es/senpy/senpy/commits/master
|
||||
|
||||
.. image:: https://img.shields.io/pypi/l/requests.svg
|
||||
:target: https://lab.gsi.upm.es/senpy/senpy/
|
||||
|
||||
Senpy lets you create sentiment analysis web services easily, fast and using a well known API.
|
||||
As a bonus, senpy services use semantic vocabularies (e.g. `NIF <http://persistence.uni-leipzig.org/nlp2rdf/>`_, `Marl <http://www.gsi.dit.upm.es/ontologies/marl>`_, `Onyx <http://www.gsi.dit.upm.es/ontologies/onyx>`_) and formats (turtle, JSON-LD, xml-rdf).
|
||||
|
||||
@ -12,7 +21,7 @@ Have you ever wanted to turn your sentiment analysis algorithms into a service?
|
||||
With senpy, now you can.
|
||||
It provides all the tools so you just have to worry about improving your algorithms:
|
||||
|
||||
`See it in action. <http://senpy.cluster.gsi.dit.upm.es/>`_
|
||||
`See it in action. <http://senpy.gsi.upm.es/>`_
|
||||
|
||||
Installation
|
||||
------------
|
||||
@ -38,9 +47,9 @@ If you want to install senpy globally, use sudo instead of the ``--user`` flag.
|
||||
|
||||
Docker Image
|
||||
************
|
||||
Build the image or use the pre-built one: ``docker run -ti -p 5000:5000 gsiupm/senpy --default-plugins``.
|
||||
Build the image or use the pre-built one: ``docker run -ti -p 5000:5000 gsiupm/senpy``.
|
||||
|
||||
To add custom plugins, add a volume and tell senpy where to find the plugins: ``docker run -ti -p 5000:5000 -v <PATH OF PLUGINS>:/plugins gsiupm/senpy --default-plugins -f /plugins``
|
||||
To add custom plugins, add a volume and tell senpy where to find the plugins: ``docker run -ti -p 5000:5000 -v <PATH OF PLUGINS>:/plugins gsiupm/senpy -f /plugins``
|
||||
|
||||
|
||||
Developing
|
||||
@ -125,6 +134,16 @@ For more information, check out the `documentation <http://senpy.readthedocs.org
|
||||
------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
Python 2.x compatibility
|
||||
------------------------
|
||||
|
||||
Keeping compatibility between python 2.7 and 3.x is not always easy, especially for a framework that deals both with text and web requests.
|
||||
Hence, starting February 2019, this project will no longer make efforts to support python 2.7, which will reach its end of life in 2020.
|
||||
Most of the functionality should still work, and the compatibility shims will remain for now, but we cannot make any guarantees at this point.
|
||||
Instead, the maintainers will focus their efforts on keeping the codebase compatible across different Python 3.3+ versions, including upcoming ones.
|
||||
We apologize for the inconvenience.
|
||||
|
||||
|
||||
Acknowledgement
|
||||
---------------
|
||||
This development has been partially funded by the European Union through the MixedEmotions Project (project number H2020 655632), as part of the `RIA ICT 15 Big data and Open Data Innovation and take-up` programme.
|
||||
|
@ -1,4 +0,0 @@
|
||||
import os
|
||||
|
||||
SERVER_PORT = os.environ.get("SERVER_PORT", 5000)
|
||||
DEBUG = os.environ.get("DEBUG", True)
|
@ -24,6 +24,7 @@ I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " entr to watch for changes and continuously make HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@ -49,6 +50,9 @@ help:
|
||||
clean:
|
||||
rm -rf $(BUILDDIR)/*
|
||||
|
||||
entr:
|
||||
while true; do ag -g rst | entr -d make html; done
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
|
4659
docs/Quickstart.ipynb
Normal file
4659
docs/Quickstart.ipynb
Normal file
File diff suppressed because it is too large
Load Diff
2599
docs/Quickstart.rst
Normal file
2599
docs/Quickstart.rst
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,317 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2017-04-10T17:05:31.465571Z",
|
||||
"start_time": "2017-04-10T19:05:31.458282+02:00"
|
||||
},
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"source": [
|
||||
"# Client"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"collapsed": true,
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"source": [
|
||||
"The built-in senpy client allows you to query any Senpy endpoint. We will illustrate how to use it with the public demo endpoint, and then show you how to spin up your own endpoint using docker."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"source": [
|
||||
"Demo Endpoint\n",
|
||||
"-------------"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"source": [
|
||||
"To start using senpy, simply create a new Client and point it to your endpoint. In this case, the latest version of Senpy at GSI."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2017-04-10T17:29:12.827640Z",
|
||||
"start_time": "2017-04-10T19:29:12.818617+02:00"
|
||||
},
|
||||
"collapsed": false,
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from senpy.client import Client\n",
|
||||
"\n",
|
||||
"c = Client('http://latest.senpy.cluster.gsi.dit.upm.es/api')\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"source": [
|
||||
"Now, let's use that client analyse some queries:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2017-04-10T17:29:14.011657Z",
|
||||
"start_time": "2017-04-10T19:29:13.701808+02:00"
|
||||
},
|
||||
"collapsed": false,
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"r = c.analyse('I like sugar!!', algorithm='sentiment140')\n",
|
||||
"r"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2017-04-10T17:08:19.616754Z",
|
||||
"start_time": "2017-04-10T19:08:19.610767+02:00"
|
||||
},
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"source": [
|
||||
"As you can see, that gave us the full JSON result. A more concise way to print it would be:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2017-04-10T17:29:14.854213Z",
|
||||
"start_time": "2017-04-10T19:29:14.842068+02:00"
|
||||
},
|
||||
"collapsed": false,
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"for entry in r.entries:\n",
|
||||
" print('{} -> {}'.format(entry['text'], entry['sentiments'][0]['marl:hasPolarity']))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"source": [
|
||||
"We can also obtain a list of available plugins with the client:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2017-04-10T17:29:16.245198Z",
|
||||
"start_time": "2017-04-10T19:29:16.056545+02:00"
|
||||
},
|
||||
"collapsed": false,
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"c.plugins()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"source": [
|
||||
"Or, more concisely:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2017-04-10T17:29:17.663275Z",
|
||||
"start_time": "2017-04-10T19:29:17.484623+02:00"
|
||||
},
|
||||
"collapsed": false,
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"c.plugins().keys()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"source": [
|
||||
"Local Endpoint\n",
|
||||
"--------------"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"source": [
|
||||
"To run your own instance of senpy, just create a docker container with the latest Senpy image. Using `--default-plugins` you will get some extra plugins to start playing with the API."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2017-04-10T17:29:20.637539Z",
|
||||
"start_time": "2017-04-10T19:29:19.938322+02:00"
|
||||
},
|
||||
"collapsed": false,
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!docker run -ti --name 'SenpyEndpoint' -d -p 6000:5000 gsiupm/senpy:0.8.6 --host 0.0.0.0 --default-plugins"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"source": [
|
||||
"To use this endpoint:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2017-04-10T17:29:21.263976Z",
|
||||
"start_time": "2017-04-10T19:29:21.260595+02:00"
|
||||
},
|
||||
"collapsed": false,
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"c_local = Client('http://127.0.0.1:6000/api')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"source": [
|
||||
"That's all! After you are done with your analysis, stop the docker container:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"ExecuteTime": {
|
||||
"end_time": "2017-04-10T17:29:33.226686Z",
|
||||
"start_time": "2017-04-10T19:29:22.392121+02:00"
|
||||
},
|
||||
"collapsed": false,
|
||||
"deletable": true,
|
||||
"editable": true
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!docker stop SenpyEndpoint\n",
|
||||
"!docker rm SenpyEndpoint"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"anaconda-cloud": {},
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.6.0"
|
||||
},
|
||||
"toc": {
|
||||
"colors": {
|
||||
"hover_highlight": "#DAA520",
|
||||
"running_highlight": "#FF0000",
|
||||
"selected_highlight": "#FFD700"
|
||||
},
|
||||
"moveMenuLeft": true,
|
||||
"nav_menu": {
|
||||
"height": "68px",
|
||||
"width": "252px"
|
||||
},
|
||||
"navigate_menu": true,
|
||||
"number_sections": true,
|
||||
"sideBar": true,
|
||||
"threshold": 4,
|
||||
"toc_cell": false,
|
||||
"toc_section_display": "block",
|
||||
"toc_window_display": false
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 1
|
||||
}
|
@ -1,106 +0,0 @@
|
||||
|
||||
Client
|
||||
======
|
||||
|
||||
Demo Endpoint
|
||||
-------------
|
||||
|
||||
Import Client and send a request
|
||||
|
||||
.. code:: python
|
||||
|
||||
from senpy.client import Client
|
||||
|
||||
c = Client('http://latest.senpy.cluster.gsi.dit.upm.es/api')
|
||||
r = c.analyse('I like Pizza', algorithm='sentiment140')
|
||||
|
||||
Print response
|
||||
|
||||
.. code:: python
|
||||
|
||||
for entry in r.entries:
|
||||
print('{} -> {}'.format(entry['text'], entry['sentiments'][0]['marl:hasPolarity']))
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
I like Pizza -> marl:Positive
|
||||
|
||||
|
||||
Obtain a list of available plugins
|
||||
|
||||
.. code:: python
|
||||
|
||||
for plugin in c.request('/plugins')['plugins']:
|
||||
print(plugin['name'])
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
emoRand
|
||||
rand
|
||||
sentiment140
|
||||
|
||||
|
||||
Local Endpoint
|
||||
--------------
|
||||
|
||||
Run a docker container with Senpy image and default plugins
|
||||
|
||||
.. code::
|
||||
|
||||
docker run -ti --name 'SenpyEndpoint' -d -p 5000:5000 gsiupm/senpy:0.8.6 --host 0.0.0.0 --default-plugins
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
a0157cd98057072388bfebeed78a830da7cf0a796f4f1a3fd9188f9f2e5fe562
|
||||
|
||||
|
||||
Import client and send a request to localhost
|
||||
|
||||
.. code:: python
|
||||
|
||||
c_local = Client('http://127.0.0.1:5000/api')
|
||||
r = c_local.analyse('Hello world', algorithm='sentiment140')
|
||||
|
||||
Print response
|
||||
|
||||
.. code:: python
|
||||
|
||||
for entry in r.entries:
|
||||
print('{} -> {}'.format(entry['text'], entry['sentiments'][0]['marl:hasPolarity']))
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
Hello world -> marl:Neutral
|
||||
|
||||
|
||||
Obtain a list of available plugins deployed locally
|
||||
|
||||
.. code:: python
|
||||
|
||||
c_local.plugins().keys()
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
rand
|
||||
sentiment140
|
||||
emoRand
|
||||
|
||||
|
||||
Stop the docker container
|
||||
|
||||
.. code:: python
|
||||
|
||||
!docker stop SenpyEndpoint
|
||||
!docker rm SenpyEndpoint
|
||||
|
||||
|
||||
.. parsed-literal::
|
||||
|
||||
SenpyEndpoint
|
||||
SenpyEndpoint
|
||||
|
@ -1,11 +0,0 @@
|
||||
About
|
||||
--------
|
||||
|
||||
If you use Senpy in your research, please cite `Senpy: A Pragmatic Linked Sentiment Analysis Framework <http://gsi.dit.upm.es/index.php/es/investigacion/publicaciones?view=publication&task=show&id=417>`__ (`BibTex <http://gsi.dit.upm.es/index.php/es/investigacion/publicaciones?controller=publications&task=export&format=bibtex&id=417>`__):
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
Sánchez-Rada, J. F., Iglesias, C. A., Corcuera, I., & Araque, Ó. (2016, October).
|
||||
Senpy: A Pragmatic Linked Sentiment Analysis Framework.
|
||||
In Data Science and Advanced Analytics (DSAA),
|
||||
2016 IEEE International Conference on (pp. 735-742). IEEE.
|
9
docs/advanced.rst
Normal file
9
docs/advanced.rst
Normal file
@ -0,0 +1,9 @@
|
||||
Advanced usage
|
||||
--------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
server-cli
|
||||
conversion
|
||||
commandline
|
17
docs/api.rst
17
docs/api.rst
@ -25,7 +25,7 @@ NIF API
|
||||
"@context":"http://127.0.0.1/api/contexts/Results.jsonld",
|
||||
"@id":"_:Results_11241245.22",
|
||||
"@type":"results"
|
||||
"analysis": [
|
||||
"activities": [
|
||||
"plugins/sentiment-140_0.1"
|
||||
],
|
||||
"entries": [
|
||||
@ -73,7 +73,7 @@ NIF API
|
||||
.. http:get:: /api/plugins
|
||||
|
||||
Returns a list of installed plugins.
|
||||
**Example request**:
|
||||
**Example request and response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
@ -82,10 +82,6 @@ NIF API
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
{
|
||||
"@id": "plugins/sentiment-140_0.1",
|
||||
"@type": "sentimentPlugin",
|
||||
@ -143,19 +139,14 @@ NIF API
|
||||
.. http:get:: /api/plugins/<pluginname>
|
||||
|
||||
Returns the information of a specific plugin.
|
||||
**Example request**:
|
||||
**Example request and response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
GET /api/plugins/rand/ HTTP/1.1
|
||||
GET /api/plugins/sentiment-random/ HTTP/1.1
|
||||
Host: localhost
|
||||
Accept: application/json, text/javascript
|
||||
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
{
|
||||
"@context": "http://127.0.0.1/api/contexts/ExamplePlugin.jsonld",
|
||||
"@id": "plugins/ExamplePlugin_0.1",
|
||||
|
@ -1,5 +1,6 @@
|
||||
API and Examples
|
||||
################
|
||||
API and vocabularies
|
||||
####################
|
||||
|
||||
.. toctree::
|
||||
|
||||
vocabularies.rst
|
||||
|
@ -2,7 +2,7 @@
|
||||
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
|
||||
"@id": "me:Result1",
|
||||
"@type": "results",
|
||||
"analysis": [
|
||||
"activities": [
|
||||
"me:SAnalysis1",
|
||||
"me:SgAnalysis1",
|
||||
"me:EmotionAnalysis1",
|
||||
|
@ -2,7 +2,7 @@
|
||||
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
|
||||
"@id": "http://example.com#NIFExample",
|
||||
"@type": "results",
|
||||
"analysis": [
|
||||
"activities": [
|
||||
],
|
||||
"entries": [
|
||||
{
|
||||
|
@ -1,7 +1,8 @@
|
||||
Command line
|
||||
============
|
||||
|
||||
This video shows how to analyse text directly on the command line using the senpy tool.
|
||||
Although the main use of senpy is to publish services, the tool can also be used locally to analyze text in the command line.
|
||||
This is a short video demonstration:
|
||||
|
||||
.. image:: https://asciinema.org/a/9uwef1ghkjk062cw2t4mhzpyk.png
|
||||
:width: 100%
|
||||
|
@ -7,9 +7,9 @@ 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
|
||||
Consider the original query: http://127.0.0.1:5000/api/?i=hello&algo=emotion-random
|
||||
|
||||
The requested plugin (emoRand) returns emotions using Ekman's model (or big6 in EmotionML):
|
||||
The requested plugin (emotion-random) returns emotions using Ekman's model (or big6 in EmotionML):
|
||||
|
||||
.. code:: json
|
||||
|
||||
@ -21,14 +21,14 @@ The requested plugin (emoRand) returns emotions using Ekman's model (or big6 in
|
||||
"@type": "emotion",
|
||||
"onyx:hasEmotionCategory": "emoml:big6anger"
|
||||
},
|
||||
"prov:wasGeneratedBy": "plugins/emoRand_0.1"
|
||||
"prov:wasGeneratedBy": "plugins/emotion-random_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
|
||||
http://127.0.0.1:5000/api/?i=hello&algo=emotion-random&emotionModel=emoml:fsre-dimensions
|
||||
|
||||
This call, provided there is a valid conversion plugin from Ekman's to VAD, would return something like this:
|
||||
|
||||
@ -42,7 +42,7 @@ This call, provided there is a valid conversion plugin from Ekman's to VAD, woul
|
||||
"@type": "emotion",
|
||||
"onyx:hasEmotionCategory": "emoml:big6anger"
|
||||
},
|
||||
"prov:wasGeneratedBy": "plugins/emoRand_0.1"
|
||||
"prov:wasGeneratedBy": "plugins/emotion-random.1"
|
||||
}, {
|
||||
"@type": "emotionSet",
|
||||
"onyx:hasEmotion": {
|
||||
@ -69,7 +69,7 @@ It is also possible to get the original emotion nested within the new converted
|
||||
"@type": "emotion",
|
||||
"onyx:hasEmotionCategory": "emoml:big6anger"
|
||||
},
|
||||
"prov:wasGeneratedBy": "plugins/emoRand_0.1"
|
||||
"prov:wasGeneratedBy": "plugins/emotion-random.1"
|
||||
"onyx:wasDerivedFrom": {
|
||||
"@type": "emotionSet",
|
||||
"onyx:hasEmotion": {
|
||||
|
@ -1,7 +1,7 @@
|
||||
Demo
|
||||
----
|
||||
|
||||
There is a demo available on http://senpy.cluster.gsi.dit.upm.es/, where you can test a serie of different plugins.
|
||||
There is a demo available on http://senpy.gsi.upm.es/, where you can test a live instance of Senpy, with several open source plugins.
|
||||
You can use the playground (a web interface) or make HTTP requests to the service API.
|
||||
|
||||
.. image:: senpy-playground.png
|
||||
@ -10,7 +10,5 @@ You can use the playground (a web interface) or make HTTP requests to the servic
|
||||
:scale: 100 %
|
||||
:align: center
|
||||
|
||||
Plugins Demo
|
||||
============
|
||||
|
||||
The source code and description of the plugins used in the demo is available here: https://lab.cluster.gsi.dit.upm.es/senpy/senpy-plugins-community/.
|
||||
The source code and description of the plugins used in the demo are available here: https://lab.gsi.upm.es/senpy/senpy-plugins-community/.
|
||||
|
27
docs/development.rst
Normal file
27
docs/development.rst
Normal file
@ -0,0 +1,27 @@
|
||||
Developing new services
|
||||
-----------------------
|
||||
|
||||
Developing web services can be hard.
|
||||
To illustrate it, the figure below summarizes the typical features in a text analysis service.
|
||||
|
||||
.. image:: senpy-framework.png
|
||||
:width: 60%
|
||||
:align: center
|
||||
|
||||
Senpy implements all the common blocks, so developers can focus on what really matters: great analysis algorithms that solve real problems.
|
||||
Among other things, Senpy takes care of these tasks:
|
||||
|
||||
* Interfacing with the user: parameter validation, error handling.
|
||||
* Formatting: JSON-LD, Turtle/n-triples input and output, or simple text input
|
||||
* Linked Data: senpy results are semantically annotated, using a series of well established vocabularies, and sane default URIs.
|
||||
* User interface: a web UI where users can explore your service and test different settings
|
||||
* A client to interact with the service. Currently only available in Python.
|
||||
|
||||
You only need to provide the algorithm to turn a piece of text into an annotation
|
||||
Sharing your sentiment analysis with the world has never been easier!
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
plugins-quickstart
|
||||
plugins-faq
|
BIN
docs/evaluation-results.png
Normal file
BIN
docs/evaluation-results.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 76 KiB |
@ -1,5 +1,6 @@
|
||||
Examples
|
||||
------
|
||||
--------
|
||||
|
||||
All the examples in this page use the :download:`the main schema <_static/schemas/definitions.json>`.
|
||||
|
||||
Simple NIF annotation
|
||||
@ -17,6 +18,7 @@ Sentiment Analysis
|
||||
.....................
|
||||
Description
|
||||
,,,,,,,,,,,
|
||||
|
||||
This annotation corresponds to the sentiment analysis of an input. The example shows the sentiment represented according to Marl format.
|
||||
The sentiments detected are contained in the Sentiments array with their related part of the text.
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
|
||||
"@id": "me:Result1",
|
||||
"@type": "results",
|
||||
"analysis": [
|
||||
"activities": [
|
||||
{
|
||||
"@id": "_:SAnalysis1_Activity",
|
||||
"@type": "marl:SentimentAnalysis",
|
||||
|
@ -2,7 +2,7 @@
|
||||
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
|
||||
"@id": "me:Result1",
|
||||
"@type": "results",
|
||||
"analysis": [ ],
|
||||
"activities": [ ],
|
||||
"entries": [
|
||||
{
|
||||
"@id": "http://example.org#char=0,40",
|
||||
|
@ -2,7 +2,7 @@
|
||||
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
|
||||
"@id": "me:Result1",
|
||||
"@type": "results",
|
||||
"analysis": [
|
||||
"activities": [
|
||||
{
|
||||
"@id": "_:SAnalysis1_Activity",
|
||||
"@type": "marl:SentimentAnalysis",
|
||||
|
@ -2,7 +2,7 @@
|
||||
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
|
||||
"@id": "me:Result1",
|
||||
"@type": "results",
|
||||
"analysis": [
|
||||
"activities": [
|
||||
{
|
||||
"@id": "me:EmotionAnalysis1_Activity",
|
||||
"@type": "me:EmotionAnalysis1",
|
||||
|
@ -2,7 +2,7 @@
|
||||
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
|
||||
"@id": "me:Result1",
|
||||
"@type": "results",
|
||||
"analysis": [
|
||||
"activities": [
|
||||
{
|
||||
"@id": "_:NER1_Activity",
|
||||
"@type": "me:NERAnalysis",
|
||||
|
@ -7,7 +7,7 @@
|
||||
],
|
||||
"@id": "me:Result1",
|
||||
"@type": "results",
|
||||
"analysis": [
|
||||
"activities": [
|
||||
{
|
||||
"@id": "me:HesamsAnalysis_Activity",
|
||||
"@type": "onyx:EmotionAnalysis",
|
||||
|
@ -2,7 +2,7 @@
|
||||
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
|
||||
"@id": "me:Result1",
|
||||
"@type": "results",
|
||||
"analysis": [
|
||||
"activities": [
|
||||
{
|
||||
"@id": "_:SAnalysis1_Activity",
|
||||
"@type": "marl:SentimentAnalysis",
|
||||
|
@ -2,7 +2,7 @@
|
||||
"@context": "http://mixedemotions-project.eu/ns/context.jsonld",
|
||||
"@id": "me:Result1",
|
||||
"@type": "results",
|
||||
"analysis": [
|
||||
"activities": [
|
||||
{
|
||||
"@id": "_:SgAnalysis1_Activity",
|
||||
"@type": "me:SuggestionAnalysis",
|
||||
|
@ -4,32 +4,31 @@ Welcome to Senpy's documentation!
|
||||
:target: http://senpy.readthedocs.io/en/latest/
|
||||
.. image:: https://badge.fury.io/py/senpy.svg
|
||||
:target: https://badge.fury.io/py/senpy
|
||||
.. image:: https://lab.cluster.gsi.dit.upm.es/senpy/senpy/badges/master/build.svg
|
||||
:target: https://lab.cluster.gsi.dit.upm.es/senpy/senpy/commits/master
|
||||
.. image:: https://lab.cluster.gsi.dit.upm.es/senpy/senpy/badges/master/coverage.svg
|
||||
:target: https://lab.cluster.gsi.dit.upm.es/senpy/senpy/commits/master
|
||||
.. image:: https://lab.gsi.upm.es/senpy/senpy/badges/master/build.svg
|
||||
:target: https://lab.gsi.upm.es/senpy/senpy/commits/master
|
||||
.. image:: https://lab.gsi.upm.es/senpy/senpy/badges/master/coverage.svg
|
||||
:target: https://lab.gsi.upm.es/senpy/senpy/commits/master
|
||||
.. image:: https://img.shields.io/pypi/l/requests.svg
|
||||
:target: https://lab.cluster.gsi.dit.upm.es/senpy/senpy/
|
||||
:target: https://lab.gsi.upm.es/senpy/senpy/
|
||||
|
||||
|
||||
|
||||
Senpy is a framework for sentiment and emotion analysis services.
|
||||
Services built with senpy are interchangeable and easy to use because they share a common :doc:`apischema`.
|
||||
It also simplifies service development.
|
||||
Senpy services are interchangeable and easy to use because they share a common semantic :doc:`apischema`.
|
||||
|
||||
.. image:: senpy-architecture.png
|
||||
:width: 100%
|
||||
:align: center
|
||||
If you interested in consuming Senpy services, read :doc:`Quickstart`.
|
||||
To get familiar with the concepts behind Senpy, and what it can offer for service developers, check out :doc:`development`.
|
||||
:doc:`apischema` contains information about the semantic models and vocabularies used by Senpy.
|
||||
|
||||
.. toctree::
|
||||
:caption: Learn more about senpy:
|
||||
:maxdepth: 2
|
||||
|
||||
senpy
|
||||
Quickstart
|
||||
installation
|
||||
demo
|
||||
usage
|
||||
development
|
||||
apischema
|
||||
plugins
|
||||
conversion
|
||||
about
|
||||
advanced
|
||||
demo
|
||||
publications
|
||||
|
@ -32,27 +32,25 @@ If you want to install senpy globally, use sudo instead of the ``--user`` flag.
|
||||
|
||||
Docker Image
|
||||
************
|
||||
Build the image or use the pre-built one:
|
||||
The base image of senpy comes with some builtin plugins that you can use:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
docker run -ti -p 5000:5000 gsiupm/senpy --host 0.0.0.0 --default-plugins
|
||||
docker run -ti -p 5000:5000 gsiupm/senpy --host 0.0.0.0
|
||||
|
||||
To add custom plugins, use a docker volume:
|
||||
To add your custom plugins, you can use a docker volume:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
docker run -ti -p 5000:5000 -v <PATH OF PLUGINS>:/plugins gsiupm/senpy --host 0.0.0.0 --default-plugins -f /plugins
|
||||
docker run -ti -p 5000:5000 -v <PATH OF PLUGINS>:/plugins gsiupm/senpy --host 0.0.0.0 --plugins -f /plugins
|
||||
|
||||
|
||||
Python 2
|
||||
........
|
||||
|
||||
There is a Senpy version for python2 too:
|
||||
There is a Senpy image for **python 2**, too:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
docker run -ti -p 5000:5000 gsiupm/senpy:python2.7 --host 0.0.0.0 --default-plugins
|
||||
docker run -ti -p 5000:5000 gsiupm/senpy:python2.7 --host 0.0.0.0
|
||||
|
||||
|
||||
Alias
|
||||
@ -62,7 +60,7 @@ If you are using the docker approach regularly, it is advisable to use a script
|
||||
|
||||
.. code:: bash
|
||||
|
||||
alias senpy='docker run --rm -ti -p 5000:5000 -v $PWD:/senpy-plugins gsiupm/senpy --default-plugins'
|
||||
alias senpy='docker run --rm -ti -p 5000:5000 -v $PWD:/senpy-plugins gsiupm/senpy'
|
||||
|
||||
|
||||
Now, you may run senpy from any folder in your computer like so:
|
||||
|
@ -110,4 +110,4 @@ Now, in a file named ``helloworld.py``:
|
||||
entry.sentiments.append(sentiment)
|
||||
yield entry
|
||||
|
||||
The complete code of the example plugin is available `here <https://lab.cluster.gsi.dit.upm.es/senpy/plugin-prueba>`__.
|
||||
The complete code of the example plugin is available `here <https://lab.gsi.upm.es/senpy/plugin-prueba>`__.
|
||||
|
@ -1,61 +1,18 @@
|
||||
Developing new plugins
|
||||
----------------------
|
||||
This document contains the minimum to get you started with developing new analysis plugin.
|
||||
For an example of conversion plugins, see :doc:`conversion`.
|
||||
For a description of definition files, see :doc:`plugins-definition`.
|
||||
|
||||
A more step-by-step tutorial with slides is available `here <https://lab.cluster.gsi.dit.upm.es/senpy/senpy-tutorial>`__
|
||||
F.A.Q.
|
||||
======
|
||||
|
||||
.. contents:: :local:
|
||||
|
||||
What is a plugin?
|
||||
=================
|
||||
|
||||
A plugin is a python object that can process entries. Given an entry, it will modify it, add annotations to it, or generate new entries.
|
||||
|
||||
|
||||
What is an entry?
|
||||
=================
|
||||
|
||||
Entries are objects that can be annotated.
|
||||
In general, they will be a piece of text.
|
||||
By default, entries are `NIF contexts <http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core/nif-core.html>`_ represented in JSON-LD format.
|
||||
It is a dictionary/JSON object that looks like this:
|
||||
|
||||
.. code:: python
|
||||
|
||||
{
|
||||
"@id": "<unique identifier or blank node name>",
|
||||
"nif:isString": "input text",
|
||||
"sentiments": [ {
|
||||
...
|
||||
}
|
||||
],
|
||||
...
|
||||
}
|
||||
|
||||
Annotations are added to the object like this:
|
||||
|
||||
.. code:: python
|
||||
|
||||
entry = Entry()
|
||||
entry.vocabulary__annotationName = 'myvalue'
|
||||
entry['vocabulary:annotationName'] = 'myvalue'
|
||||
entry['annotationNameURI'] = 'myvalue'
|
||||
|
||||
Where vocabulary is one of the prefixes defined in the default senpy context, and annotationURI is a full URI.
|
||||
The value may be any valid JSON-LD dictionary.
|
||||
For simplicity, senpy includes a series of models by default in the ``senpy.models`` module.
|
||||
|
||||
|
||||
What are annotations?
|
||||
=====================
|
||||
#####################
|
||||
They are objects just like entries.
|
||||
Senpy ships with several default annotations, including: ``Sentiment``, ``Emotion``, ``EmotionSet``...jk bb
|
||||
|
||||
|
||||
What's a plugin made of?
|
||||
========================
|
||||
########################
|
||||
|
||||
When receiving a query, senpy selects what plugin or plugins should process each entry, and in what order.
|
||||
It also makes sure the every entry and the parameters provided by the user meet the plugin requirements.
|
||||
@ -73,37 +30,25 @@ In practice, this is what a plugin looks like, tests included:
|
||||
The lines highlighted contain some information about the plugin.
|
||||
In particular, the following information is mandatory:
|
||||
|
||||
* A unique name for the class. In our example, Rand.
|
||||
* A unique name for the class. In our example, sentiment-random.
|
||||
* The subclass/type of plugin. This is typically either `SentimentPlugin` or `EmotionPlugin`. However, new types of plugin can be created for different annotations. The only requirement is that these new types inherit from `senpy.Analysis`
|
||||
* A description of the plugin. This can be done simply by adding a doc to the class.
|
||||
* A version, which should get updated.
|
||||
* An author name.
|
||||
|
||||
|
||||
Plugins Code
|
||||
============
|
||||
|
||||
The basic methods in a plugin are:
|
||||
|
||||
* analyse_entry: called in every user requests. It takes two parameters: ``Entry``, the entry object, and ``params``, the parameters supplied by the user. It should yield one or more ``Entry`` objects.
|
||||
* activate: used to load memory-hungry resources. For instance, to train a classifier.
|
||||
* deactivate: used to free up resources when the plugin is no longer needed.
|
||||
|
||||
Plugins are loaded asynchronously, so don't worry if the activate method takes too long. The plugin will be marked as activated once it is finished executing the method.
|
||||
|
||||
|
||||
How does senpy find modules?
|
||||
============================
|
||||
############################
|
||||
|
||||
Senpy looks for files of two types:
|
||||
|
||||
* Python files of the form `senpy_<NAME>.py` or `<NAME>_plugin.py`. In these files, it will look for: 1) Instances that inherit from `senpy.Plugin`, or subclasses of `senpy.Plugin` that can be initialized without a configuration file. i.e. classes that contain all the required attributes for a plugin.
|
||||
* Plugin definition files (see :doc:`advanced-plugins`)
|
||||
* Plugin definition files (see :doc:`plugins-definition`)
|
||||
|
||||
Defining additional parameters
|
||||
==============================
|
||||
How can I define additional parameters for my plugin?
|
||||
#####################################################
|
||||
|
||||
Your plugin may ask for additional parameters from the users of the service by using the attribute ``extra_params`` in your plugin definition.
|
||||
Your plugin may ask for additional parameters from users by using the attribute ``extra_params`` in your plugin definition.
|
||||
It takes a dictionary, where the keys are the name of the argument/parameter, and the value has the following fields:
|
||||
|
||||
* aliases: the different names which can be used in the request to use the parameter.
|
||||
@ -124,8 +69,8 @@ It takes a dictionary, where the keys are the name of the argument/parameter, an
|
||||
|
||||
|
||||
|
||||
Loading data and files
|
||||
======================
|
||||
How should I load external data and files
|
||||
#########################################
|
||||
|
||||
Most plugins will need access to files (dictionaries, lexicons, etc.).
|
||||
These files are usually heavy or under a license that does not allow redistribution.
|
||||
@ -144,7 +89,7 @@ Plugins have a convenience function `self.open` which will automatically prepend
|
||||
file_in_data = <FILE PATH>
|
||||
file_in_sources = <FILE PATH>
|
||||
|
||||
def activate(self):
|
||||
def on activate(self):
|
||||
with self.open(self.file_in_data) as f:
|
||||
self._classifier = train_from_file(f)
|
||||
file_in_source = os.path.join(self.get_folder(), self.file_in_sources)
|
||||
@ -155,8 +100,8 @@ Plugins have a convenience function `self.open` which will automatically prepend
|
||||
It is good practice to specify the paths of these files in the plugin configuration, so the same code can be reused with different resources.
|
||||
|
||||
|
||||
Docker image
|
||||
============
|
||||
Can I build a docker image for my plugin?
|
||||
#########################################
|
||||
|
||||
Add the following dockerfile to your project to generate a docker image with your plugin:
|
||||
|
||||
@ -204,17 +149,15 @@ Adding data to the image:
|
||||
FROM gsiupm/senpy:1.0.1
|
||||
COPY data /
|
||||
|
||||
F.A.Q.
|
||||
======
|
||||
What annotations can I use?
|
||||
???????????????????????????
|
||||
###########################
|
||||
|
||||
You can add almost any annotation to an entry.
|
||||
The most common use cases are covered in the :doc:`apischema`.
|
||||
|
||||
|
||||
Why does the analyse function yield instead of return?
|
||||
??????????????????????????????????????????????????????
|
||||
######################################################
|
||||
|
||||
This is so that plugins may add new entries to the response or filter some of them.
|
||||
For instance, a chunker may split one entry into several.
|
||||
@ -222,7 +165,7 @@ On the other hand, a conversion plugin may leave out those entries that do not c
|
||||
|
||||
|
||||
If I'm using a classifier, where should I train it?
|
||||
???????????????????????????????????????????????????
|
||||
###################################################
|
||||
|
||||
Training a classifier can be time time consuming. To avoid running the training unnecessarily, you can use ShelfMixin to store the classifier. For instance:
|
||||
|
||||
@ -256,7 +199,7 @@ A corrupt shelf prevents the plugin from loading.
|
||||
If you do not care about the data in the shelf, you can force your plugin to remove the corrupted file and load anyway, set the 'force_shelf' to True in your plugin and start it again.
|
||||
|
||||
How can I turn an external service into a plugin?
|
||||
?????????????????????????????????????????????????
|
||||
#################################################
|
||||
|
||||
This example ilustrate how to implement a plugin that accesses the Sentiment140 service.
|
||||
|
||||
@ -292,8 +235,8 @@ This example ilustrate how to implement a plugin that accesses the Sentiment140
|
||||
yield entry
|
||||
|
||||
|
||||
Can I activate a DEBUG mode for my plugin?
|
||||
???????????????????????????????????????????
|
||||
How can I activate a DEBUG mode for my plugin?
|
||||
###############################################
|
||||
|
||||
You can activate the DEBUG mode by the command-line tool using the option -d.
|
||||
|
||||
@ -309,6 +252,6 @@ Additionally, with the ``--pdb`` option you will be dropped into a pdb post mort
|
||||
python -m pdb yourplugin.py
|
||||
|
||||
Where can I find more code examples?
|
||||
????????????????????????????????????
|
||||
####################################
|
||||
|
||||
See: `<http://github.com/gsi-upm/senpy-plugins-community>`_.
|
86
docs/plugins-quickstart.rst
Normal file
86
docs/plugins-quickstart.rst
Normal file
@ -0,0 +1,86 @@
|
||||
Quickstart for service developers
|
||||
=================================
|
||||
|
||||
This document contains the minimum to get you started with developing new services using Senpy.
|
||||
|
||||
For an example of conversion plugins, see :doc:`conversion`.
|
||||
For a description of definition files, see :doc:`plugins-definition`.
|
||||
|
||||
A more step-by-step tutorial with slides is available `here <https://lab.gsi.upm.es/senpy/senpy-tutorial>`__
|
||||
|
||||
.. contents:: :local:
|
||||
|
||||
Installation
|
||||
############
|
||||
|
||||
First of all, you need to install the package.
|
||||
See :doc:`installation` for instructions.
|
||||
Once installed, the `senpy` command should be available.
|
||||
|
||||
Architecture
|
||||
############
|
||||
|
||||
The main component of a sentiment analysis service is the algorithm itself. However, for the algorithm to work, it needs to get the appropriate parameters from the user, format the results according to the defined API, interact with the user whn errors occur or more information is needed, etc.
|
||||
|
||||
Senpy proposes a modular and dynamic architecture that allows:
|
||||
|
||||
* Implementing different algorithms in a extensible way, yet offering a common interface.
|
||||
* Offering common services that facilitate development, so developers can focus on implementing new and better algorithms.
|
||||
|
||||
The framework consists of two main modules: Senpy core, which is the building block of the service, and Senpy plugins, which consist of the analysis algorithm. The next figure depicts a simplified version of the processes involved in an analysis with the Senpy framework.
|
||||
|
||||
.. image:: senpy-architecture.png
|
||||
:width: 100%
|
||||
:align: center
|
||||
|
||||
|
||||
What is a plugin?
|
||||
#################
|
||||
|
||||
A plugin is a python object that can process entries. Given an entry, it will modify it, add annotations to it, or generate new entries.
|
||||
|
||||
|
||||
What is an entry?
|
||||
#################
|
||||
|
||||
Entries are objects that can be annotated.
|
||||
In general, they will be a piece of text.
|
||||
By default, entries are `NIF contexts <http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core/nif-core.html>`_ represented in JSON-LD format.
|
||||
It is a dictionary/JSON object that looks like this:
|
||||
|
||||
.. code:: python
|
||||
|
||||
{
|
||||
"@id": "<unique identifier or blank node name>",
|
||||
"nif:isString": "input text",
|
||||
"sentiments": [ {
|
||||
...
|
||||
}
|
||||
],
|
||||
...
|
||||
}
|
||||
|
||||
Annotations are added to the object like this:
|
||||
|
||||
.. code:: python
|
||||
|
||||
entry = Entry()
|
||||
entry.vocabulary__annotationName = 'myvalue'
|
||||
entry['vocabulary:annotationName'] = 'myvalue'
|
||||
entry['annotationNameURI'] = 'myvalue'
|
||||
|
||||
Where vocabulary is one of the prefixes defined in the default senpy context, and annotationURI is a full URI.
|
||||
The value may be any valid JSON-LD dictionary.
|
||||
For simplicity, senpy includes a series of models by default in the ``senpy.models`` module.
|
||||
|
||||
Plugins Code
|
||||
############
|
||||
|
||||
The basic methods in a plugin are:
|
||||
|
||||
* analyse_entry: called in every user requests. It takes two parameters: ``Entry``, the entry object, and ``params``, the parameters supplied by the user. It should yield one or more ``Entry`` objects.
|
||||
* activate: used to load memory-hungry resources. For instance, to train a classifier.
|
||||
* deactivate: used to free up resources when the plugin is no longer needed.
|
||||
|
||||
Plugins are loaded asynchronously, so don't worry if the activate method takes too long. The plugin will be marked as activated once it is finished executing the method.
|
||||
|
46
docs/publications.rst
Normal file
46
docs/publications.rst
Normal file
@ -0,0 +1,46 @@
|
||||
Publications
|
||||
============
|
||||
|
||||
|
||||
If you use Senpy in your research, please cite `Senpy: A Pragmatic Linked Sentiment Analysis Framework <http://gsi.dit.upm.es/index.php/es/investigacion/publicaciones?view=publication&task=show&id=417>`__ (`BibTex <http://gsi.dit.upm.es/index.php/es/investigacion/publicaciones?controller=publications&task=export&format=bibtex&id=417>`__):
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
Sánchez-Rada, J. F., Iglesias, C. A., Corcuera, I., & Araque, Ó. (2016, October).
|
||||
Senpy: A Pragmatic Linked Sentiment Analysis Framework.
|
||||
In Data Science and Advanced Analytics (DSAA),
|
||||
2016 IEEE International Conference on (pp. 735-742). IEEE.
|
||||
|
||||
|
||||
|
||||
Senpy uses Onyx for emotion representation, first introduced in:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
Sánchez-Rada, J. F., & Iglesias, C. A. (2016).
|
||||
Onyx: A linked data approach to emotion representation.
|
||||
Information Processing & Management, 52(1), 99-114.
|
||||
|
||||
Senpy uses Marl for sentiment representation, which was presented in:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
Westerski, A., Iglesias Fernandez, C. A., & Tapia Rico, F. (2011).
|
||||
Linked opinions: Describing sentiments on the structured web of data.
|
||||
|
||||
|
||||
Senpy has been used extensively in the toolbox of the MixedEmotions project:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
Buitelaar, P., Wood, I. D., Arcan, M., McCrae, J. P., Abele, A., Robin, C., … Tummarello, G. (2018).
|
||||
MixedEmotions: An Open-Source Toolbox for Multi-Modal Emotion Analysis.
|
||||
IEEE Transactions on Multimedia.
|
||||
|
||||
The representation models, formats and challenges are partially covered in a chapter of the book Sentiment Analysis in Social Networks:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
Iglesias, C. A., Sánchez-Rada, J. F., Vulcu, G., & Buitelaar, P. (2017).
|
||||
Linked Data Models for Sentiment and Emotion Analysis in Social Networks.
|
||||
In Sentiment Analysis in Social Networks (pp. 49-69).
|
@ -1,54 +1,27 @@
|
||||
What is Senpy?
|
||||
--------------
|
||||
|
||||
Senpy is a framework for text analysis using Linked Data. There are three main applications of Senpy so far: sentiment and emotion analysis, user profiling and entity recoginition. Annotations and Services are compliant with NIF (NLP Interchange Format).
|
||||
|
||||
Senpy aims at providing a framework where analysis modules can be integrated easily as plugins, and providing a core functionality for managing tasks such as data validation, user interaction, formatting, logging, translation to linked data, etc.
|
||||
|
||||
The figure below summarizes the typical features in a text analysis service.
|
||||
Senpy implements all the common blocks, so developers can focus on what really matters: great analysis algorithms that solve real problems.
|
||||
|
||||
.. image:: senpy-framework.png
|
||||
:width: 60%
|
||||
:align: center
|
||||
|
||||
|
||||
Senpy for end users
|
||||
===================
|
||||
|
||||
All services built using senpy share a common interface.
|
||||
This allows users to use them (almost) interchangeably.
|
||||
Senpy comes with a :ref:`built-in client`.
|
||||
|
||||
|
||||
Senpy for service developers
|
||||
============================
|
||||
|
||||
Senpy is a framework that turns your sentiment or emotion analysis algorithm into a full blown semantic service.
|
||||
Senpy takes care of:
|
||||
|
||||
* Interfacing with the user: parameter validation, error handling.
|
||||
* Formatting: JSON-LD, Turtle/n-triples input and output, or simple text input
|
||||
* Linked Data: senpy results are semantically annotated, using a series of well established vocabularies, and sane default URIs.
|
||||
* User interface: a web UI where users can explore your service and test different settings
|
||||
* A client to interact with the service. Currently only available in Python.
|
||||
|
||||
Sharing your sentiment analysis with the world has never been easier!
|
||||
|
||||
Check out the :doc:`plugins` if you have developed an analysis algorithm (e.g. sentiment analysis) and you want to publish it as a service.
|
||||
|
||||
Architecture
|
||||
============
|
||||
|
||||
The main component of a sentiment analysis service is the algorithm itself. However, for the algorithm to work, it needs to get the appropriate parameters from the user, format the results according to the defined API, interact with the user whn errors occur or more information is needed, etc.
|
||||
|
||||
Senpy proposes a modular and dynamic architecture that allows:
|
||||
|
||||
* Implementing different algorithms in a extensible way, yet offering a common interface.
|
||||
* Offering common services that facilitate development, so developers can focus on implementing new and better algorithms.
|
||||
|
||||
The framework consists of two main modules: Senpy core, which is the building block of the service, and Senpy plugins, which consist of the analysis algorithm. The next figure depicts a simplified version of the processes involved in an analysis with the Senpy framework.
|
||||
Senpy is a framework for sentiment and emotion analysis services.
|
||||
Its goal is to produce analysis services that are interchangeable and fully interoperable.
|
||||
|
||||
.. image:: senpy-architecture.png
|
||||
:width: 100%
|
||||
:align: center
|
||||
|
||||
All services built using senpy share a common interface.
|
||||
This allows users to use them (almost) interchangeably, with the same API and tools, simply by pointing to a different URL or changing a parameter.
|
||||
The common schema also makes it easier to evaluate the performance of different algorithms and services.
|
||||
In fact, Senpy has a built-in evaluation API you can use to compare results with different algorithms.
|
||||
|
||||
Services can also use the common interface to communicate with each other.
|
||||
And higher level features can be built on top of these services, such as automatic fusion of results, emotion model conversion, and service discovery.
|
||||
|
||||
These benefits are not limited to new services.
|
||||
The community has developed wrappers for some proprietary and commercial services (such as sentiment140 and Meaning Cloud), so you can consult them as.
|
||||
Senpy comes with a :ref:`built-in client`.
|
||||
|
||||
|
||||
To achieve this goal, Senpy uses a Linked Data principled approach, based on the NIF (NLP Interchange Format) specification, and open vocabularies such as Marl and Onyx.
|
||||
You can learn more about this in :doc:`vocabularies`.
|
||||
|
||||
Check out :doc:`plugins` if you have developed an analysis algorithm (e.g. sentiment analysis) and you want to publish it as a service.
|
||||
|
@ -5,10 +5,11 @@ The senpy server is launched via the `senpy` command:
|
||||
|
||||
.. code:: text
|
||||
|
||||
usage: senpy [-h] [--level logging_level] [--debug] [--default-plugins]
|
||||
[--host HOST] [--port PORT] [--plugins-folder PLUGINS_FOLDER]
|
||||
[--only-install] [--only-list] [--data-folder DATA_FOLDER]
|
||||
[--threaded] [--version]
|
||||
usage: senpy [-h] [--level logging_level] [--log-format log_format] [--debug]
|
||||
[--no-default-plugins] [--host HOST] [--port PORT]
|
||||
[--plugins-folder PLUGINS_FOLDER] [--only-install] [--only-test]
|
||||
[--test] [--only-list] [--data-folder DATA_FOLDER]
|
||||
[--no-threaded] [--no-deps] [--version] [--allow-fail]
|
||||
|
||||
Run a Senpy server
|
||||
|
||||
@ -16,20 +17,25 @@ The senpy server is launched via the `senpy` command:
|
||||
-h, --help show this help message and exit
|
||||
--level logging_level, -l logging_level
|
||||
Logging level
|
||||
--log-format log_format
|
||||
Logging format
|
||||
--debug, -d Run the application in debug mode
|
||||
--default-plugins Load the default plugins
|
||||
--no-default-plugins Do not load the default plugins
|
||||
--host HOST Use 0.0.0.0 to accept requests from any host.
|
||||
--port PORT, -p PORT Port to listen on.
|
||||
--plugins-folder PLUGINS_FOLDER, -f PLUGINS_FOLDER
|
||||
Where to look for plugins.
|
||||
--only-install, -i Do not run a server, only install plugin dependencies
|
||||
--only-test Do not run a server, just test all plugins
|
||||
--test, -t Test all plugins before launching the server
|
||||
--only-list, --list Do not run a server, only list plugins found
|
||||
--data-folder DATA_FOLDER, --data DATA_FOLDER
|
||||
Where to look for data. It be set with the SENPY_DATA
|
||||
environment variable as well.
|
||||
--threaded Run a threaded server
|
||||
--no-threaded Run the server without threading
|
||||
--no-deps, -n Skip installing dependencies
|
||||
--version, -v Output the senpy version and exit
|
||||
|
||||
--allow-fail, --fail Do not exit if some plugins fail to activate
|
||||
|
||||
|
||||
When launched, the server will recursively look for plugins in the specified plugins folder (the current working directory by default).
|
||||
@ -40,9 +46,9 @@ Let's run senpy with the default plugins:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
senpy -f . --default-plugins
|
||||
senpy -f .
|
||||
|
||||
Now go to `http://localhost:5000 <http://localhost:5000>`_, you should be greeted by the senpy playground:
|
||||
Now open your browser and go to `http://localhost:5000 <http://localhost:5000>`_, where you should be greeted by the senpy playground:
|
||||
|
||||
.. image:: senpy-playground.png
|
||||
:width: 100%
|
||||
@ -51,9 +57,9 @@ Now go to `http://localhost:5000 <http://localhost:5000>`_, you should be greete
|
||||
The playground is a user-friendly way to test your plugins, but you can always use the service directly: `http://localhost:5000/api?input=hello <http://localhost:5000/api?input=hello>`_.
|
||||
|
||||
|
||||
By default, senpy will listen only on the `127.0.0.1` address.
|
||||
That means you can only access the API from your (or localhost).
|
||||
You can listen on a different address using the `--host` flag (e.g., 0.0.0.0).
|
||||
By default, senpy will listen only on `127.0.0.1`.
|
||||
That means you can only access the API from your PC (i.e. localhost).
|
||||
You can listen on a different address using the `--host` flag (e.g., 0.0.0.0, to allow any computer to access it).
|
||||
The default port is 5000.
|
||||
You can change it with the `--port` flag.
|
||||
|
@ -1,15 +0,0 @@
|
||||
Usage
|
||||
-----
|
||||
|
||||
First of all, you need to install the package.
|
||||
See :doc:`installation` for instructions.
|
||||
Once installed, the `senpy` command should be available.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
server
|
||||
SenpyClientUse
|
||||
commandline
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
This is a collection of plugins that exemplify certain aspects of plugin development with senpy.
|
||||
|
||||
The first series of plugins the `basic` ones.
|
||||
The first series of plugins are the `basic` ones.
|
||||
Their starting point is a classification function defined in `basic.py`.
|
||||
They all include testing and running them as a script will run all tests.
|
||||
In ascending order of customization, the plugins are:
|
||||
@ -19,5 +19,5 @@ In rest of the plugins show advanced topics:
|
||||
|
||||
All of the plugins in this folder include a set of test cases and they are periodically tested with the latest version of senpy.
|
||||
|
||||
Additioanlly, for an example of stand-alone plugin that can be tested and deployed with docker, take a look at: lab.cluster.gsi.dit.upm.es/senpy/plugin-example
|
||||
Additioanlly, for an example of stand-alone plugin that can be tested and deployed with docker, take a look at: lab.gsi.upm.es/senpy/plugin-example
|
||||
bbm
|
||||
|
@ -1,5 +1,5 @@
|
||||
#!/usr/local/bin/python
|
||||
# coding: utf-8
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
emoticons = {
|
||||
'pos': [':)', ':]', '=)', ':D'],
|
||||
@ -7,17 +7,19 @@ emoticons = {
|
||||
}
|
||||
|
||||
emojis = {
|
||||
'pos': ['😁', '😂', '😃', '😄', '😆', '😅', '😄' '😍'],
|
||||
'neg': ['😢', '😡', '😠', '😞', '😖', '😔', '😓', '😒']
|
||||
'pos': [u'😁', u'😂', u'😃', u'😄', u'😆', u'😅', u'😄', u'😍'],
|
||||
'neg': [u'😢', u'😡', u'😠', u'😞', u'😖', u'😔', u'😓', u'😒']
|
||||
}
|
||||
|
||||
|
||||
def get_polarity(text, dictionaries=[emoticons, emojis]):
|
||||
polarity = 'marl:Neutral'
|
||||
print('Input for get_polarity', text)
|
||||
for dictionary in dictionaries:
|
||||
for label, values in dictionary.items():
|
||||
for emoticon in values:
|
||||
if emoticon and emoticon in text:
|
||||
polarity = label
|
||||
break
|
||||
print('Polarity', polarity)
|
||||
return polarity
|
||||
|
@ -1,5 +1,5 @@
|
||||
#!/usr/local/bin/python
|
||||
# coding: utf-8
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from senpy import easy_test, models, plugins
|
||||
|
||||
@ -18,13 +18,13 @@ class BasicAnalyseEntry(plugins.SentimentPlugin):
|
||||
'default': 'marl:Neutral'
|
||||
}
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
def analyse_entry(self, entry, activity):
|
||||
polarity = basic.get_polarity(entry.text)
|
||||
|
||||
polarity = self.mappings.get(polarity, self.mappings['default'])
|
||||
|
||||
s = models.Sentiment(marl__hasPolarity=polarity)
|
||||
s.prov(self)
|
||||
s.prov(activity)
|
||||
entry.sentiments.append(s)
|
||||
yield entry
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
#!/usr/local/bin/python
|
||||
# coding: utf-8
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from senpy import easy_test, SentimentBox
|
||||
|
||||
@ -12,15 +12,13 @@ class BasicBox(SentimentBox):
|
||||
author = '@balkian'
|
||||
version = '0.1'
|
||||
|
||||
mappings = {
|
||||
'pos': 'marl:Positive',
|
||||
'neg': 'marl:Negative',
|
||||
'default': 'marl:Neutral'
|
||||
}
|
||||
|
||||
def predict_one(self, input):
|
||||
output = basic.get_polarity(input)
|
||||
return self.mappings.get(output, self.mappings['default'])
|
||||
def predict_one(self, features, **kwargs):
|
||||
output = basic.get_polarity(features[0])
|
||||
if output == 'pos':
|
||||
return [1, 0, 0]
|
||||
if output == 'neg':
|
||||
return [0, 0, 1]
|
||||
return [0, 1, 0]
|
||||
|
||||
test_cases = [{
|
||||
'input': 'Hello :)',
|
||||
|
@ -1,37 +1,36 @@
|
||||
#!/usr/local/bin/python
|
||||
# coding: utf-8
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from senpy import easy_test, SentimentBox, MappingMixin
|
||||
from senpy import easy_test, SentimentBox
|
||||
|
||||
import basic
|
||||
|
||||
|
||||
class Basic(MappingMixin, SentimentBox):
|
||||
class Basic(SentimentBox):
|
||||
'''Provides sentiment annotation using a lexicon'''
|
||||
|
||||
author = '@balkian'
|
||||
version = '0.1'
|
||||
|
||||
mappings = {
|
||||
'pos': 'marl:Positive',
|
||||
'neg': 'marl:Negative',
|
||||
'default': 'marl:Neutral'
|
||||
}
|
||||
|
||||
def predict_one(self, input):
|
||||
return basic.get_polarity(input)
|
||||
def predict_one(self, features, **kwargs):
|
||||
output = basic.get_polarity(features[0])
|
||||
if output == 'pos':
|
||||
return [1, 0, 0]
|
||||
if output == 'neu':
|
||||
return [0, 1, 0]
|
||||
return [0, 0, 1]
|
||||
|
||||
test_cases = [{
|
||||
'input': 'Hello :)',
|
||||
'input': u'Hello :)',
|
||||
'polarity': 'marl:Positive'
|
||||
}, {
|
||||
'input': 'So sad :(',
|
||||
'input': u'So sad :(',
|
||||
'polarity': 'marl:Negative'
|
||||
}, {
|
||||
'input': 'Yay! Emojis 😁',
|
||||
'input': u'Yay! Emojis 😁',
|
||||
'polarity': 'marl:Positive'
|
||||
}, {
|
||||
'input': 'But no emoticons 😢',
|
||||
'input': u'But no emoticons 😢',
|
||||
'polarity': 'marl:Negative'
|
||||
}]
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
#!/usr/local/bin/python
|
||||
# coding: utf-8
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from senpy import easy_test, models, plugins
|
||||
|
||||
@ -16,7 +16,7 @@ class Dictionary(plugins.SentimentPlugin):
|
||||
|
||||
mappings = {'pos': 'marl:Positive', 'neg': 'marl:Negative'}
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
def analyse_entry(self, entry, *args, **kwargs):
|
||||
polarity = basic.get_polarity(entry.text, self.dictionaries)
|
||||
if polarity in self.mappings:
|
||||
polarity = self.mappings[polarity]
|
||||
|
@ -6,12 +6,13 @@ from senpy.models import EmotionSet, Emotion, Entry
|
||||
|
||||
class EmoRand(EmotionPlugin):
|
||||
'''A sample plugin that returns a random emotion annotation'''
|
||||
name = 'emotion-random'
|
||||
author = '@balkian'
|
||||
version = '0.1'
|
||||
url = "https://github.com/gsi-upm/senpy-plugins-community"
|
||||
onyx__usesEmotionModel = "emoml:big6"
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
def analyse_entry(self, entry, activity):
|
||||
category = "emoml:big6happiness"
|
||||
number = max(-1, min(1, random.gauss(0, 0.5)))
|
||||
if number > 0:
|
||||
@ -19,7 +20,7 @@ class EmoRand(EmotionPlugin):
|
||||
emotionSet = EmotionSet()
|
||||
emotion = Emotion({"onyx:hasEmotionCategory": category})
|
||||
emotionSet.onyx__hasEmotion.append(emotion)
|
||||
emotionSet.prov__wasGeneratedBy = self.id
|
||||
emotionSet.prov(activity)
|
||||
entry.emotions.append(emotionSet)
|
||||
yield entry
|
||||
|
||||
@ -27,6 +28,6 @@ class EmoRand(EmotionPlugin):
|
||||
params = dict()
|
||||
results = list()
|
||||
for i in range(100):
|
||||
res = next(self.analyse_entry(Entry(nif__isString="Hello"), params))
|
||||
res = next(self.analyse_entry(Entry(nif__isString="Hello"), self.activity(params)))
|
||||
res.validate()
|
||||
results.append(res.emotions[0]['onyx:hasEmotion'][0]['onyx:hasEmotionCategory'])
|
@ -1,5 +1,5 @@
|
||||
#!/usr/local/bin/python
|
||||
# coding: utf-8
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from senpy import easy_test, models, plugins
|
||||
|
||||
@ -25,7 +25,8 @@ class ParameterizedDictionary(plugins.SentimentPlugin):
|
||||
}
|
||||
}
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
def analyse_entry(self, entry, activity):
|
||||
params = activity.params
|
||||
positive_words = params['positive-words'].split(',')
|
||||
negative_words = params['negative-words'].split(',')
|
||||
dictionary = {
|
||||
@ -35,7 +36,7 @@ class ParameterizedDictionary(plugins.SentimentPlugin):
|
||||
polarity = basic.get_polarity(entry.text, [dictionary])
|
||||
|
||||
s = models.Sentiment(marl__hasPolarity=polarity)
|
||||
s.prov(self)
|
||||
s.prov(activity)
|
||||
entry.sentiments.append(s)
|
||||
yield entry
|
||||
|
||||
|
@ -2,15 +2,16 @@ import random
|
||||
from senpy import SentimentPlugin, Sentiment, Entry
|
||||
|
||||
|
||||
class Rand(SentimentPlugin):
|
||||
class RandSent(SentimentPlugin):
|
||||
'''A sample plugin that returns a random sentiment annotation'''
|
||||
name = 'sentiment-random'
|
||||
author = "@balkian"
|
||||
version = '0.1'
|
||||
url = "https://github.com/gsi-upm/senpy-plugins-community"
|
||||
marl__maxPolarityValue = '1'
|
||||
marl__minPolarityValue = "-1"
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
def analyse_entry(self, entry, activity):
|
||||
polarity_value = max(-1, min(1, random.gauss(0.2, 0.2)))
|
||||
polarity = "marl:Neutral"
|
||||
if polarity_value > 0:
|
||||
@ -19,7 +20,7 @@ class Rand(SentimentPlugin):
|
||||
polarity = "marl:Negative"
|
||||
sentiment = Sentiment(marl__hasPolarity=polarity,
|
||||
marl__polarityValue=polarity_value)
|
||||
sentiment.prov(self)
|
||||
sentiment.prov(activity)
|
||||
entry.sentiments.append(sentiment)
|
||||
yield entry
|
||||
|
||||
@ -28,8 +29,9 @@ class Rand(SentimentPlugin):
|
||||
params = dict()
|
||||
results = list()
|
||||
for i in range(50):
|
||||
activity = self.activity(params)
|
||||
res = next(self.analyse_entry(Entry(nif__isString="Hello"),
|
||||
params))
|
||||
activity))
|
||||
res.validate()
|
||||
results.append(res.sentiments[0]['marl:hasPolarity'])
|
||||
assert 'marl:Positive' in results
|
@ -1,25 +1,20 @@
|
||||
from senpy import SentimentBox, MappingMixin, easy_test
|
||||
from senpy import SentimentBox, easy_test
|
||||
|
||||
from mypipeline import pipeline
|
||||
|
||||
|
||||
class PipelineSentiment(MappingMixin, SentimentBox):
|
||||
'''
|
||||
This is a pipeline plugin that wraps a classifier defined in another module
|
||||
(mypipeline).
|
||||
'''
|
||||
class PipelineSentiment(SentimentBox):
|
||||
'''This is a pipeline plugin that wraps a classifier defined in another module
|
||||
(mypipeline).'''
|
||||
author = '@balkian'
|
||||
version = 0.1
|
||||
maxPolarityValue = 1
|
||||
minPolarityValue = -1
|
||||
|
||||
mappings = {
|
||||
1: 'marl:Positive',
|
||||
-1: 'marl:Negative'
|
||||
}
|
||||
|
||||
def predict_one(self, input):
|
||||
return pipeline.predict([input, ])[0]
|
||||
def predict_one(self, features, **kwargs):
|
||||
if pipeline.predict(features) > 0:
|
||||
return [1, 0, 0]
|
||||
return [0, 0, 1]
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
|
@ -1 +1 @@
|
||||
gsitk
|
||||
gsitk>0.1.9.1
|
||||
|
@ -15,8 +15,6 @@ spec:
|
||||
- name: senpy-latest
|
||||
image: $IMAGEWTAG
|
||||
imagePullPolicy: Always
|
||||
args:
|
||||
- "--default-plugins"
|
||||
resources:
|
||||
limits:
|
||||
memory: "512Mi"
|
||||
|
@ -12,3 +12,10 @@ spec:
|
||||
backend:
|
||||
serviceName: senpy-latest
|
||||
servicePort: 5000
|
||||
- host: latest.senpy.gsi.upm.es
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
backend:
|
||||
serviceName: senpy-latest
|
||||
servicePort: 5000
|
||||
|
@ -11,5 +11,6 @@ rdflib
|
||||
rdflib-jsonld
|
||||
numpy
|
||||
scipy
|
||||
scikit-learn
|
||||
scikit-learn>=0.20
|
||||
responses
|
||||
jmespath
|
||||
|
@ -40,8 +40,14 @@ def main():
|
||||
'-l',
|
||||
metavar='logging_level',
|
||||
type=str,
|
||||
default="WARN",
|
||||
default="INFO",
|
||||
help='Logging level')
|
||||
parser.add_argument(
|
||||
'--log-format',
|
||||
metavar='log_format',
|
||||
type=str,
|
||||
default='%(asctime)s %(levelname)-10s %(name)-30s \t %(message)s',
|
||||
help='Logging format')
|
||||
parser.add_argument(
|
||||
'--debug',
|
||||
'-d',
|
||||
@ -49,10 +55,10 @@ def main():
|
||||
default=False,
|
||||
help='Run the application in debug mode')
|
||||
parser.add_argument(
|
||||
'--default-plugins',
|
||||
'--no-default-plugins',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Load the default plugins')
|
||||
help='Do not load the default plugins')
|
||||
parser.add_argument(
|
||||
'--host',
|
||||
type=str,
|
||||
@ -68,7 +74,7 @@ def main():
|
||||
'--plugins-folder',
|
||||
'-f',
|
||||
type=str,
|
||||
default='.',
|
||||
action='append',
|
||||
help='Where to look for plugins.')
|
||||
parser.add_argument(
|
||||
'--only-install',
|
||||
@ -100,10 +106,10 @@ def main():
|
||||
default=None,
|
||||
help='Where to look for data. It be set with the SENPY_DATA environment variable as well.')
|
||||
parser.add_argument(
|
||||
'--threaded',
|
||||
action='store_false',
|
||||
default=True,
|
||||
help='Run a threaded server')
|
||||
'--no-threaded',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Run a single-threaded server')
|
||||
parser.add_argument(
|
||||
'--no-deps',
|
||||
'-n',
|
||||
@ -123,30 +129,42 @@ def main():
|
||||
default=False,
|
||||
help='Do not exit if some plugins fail to activate')
|
||||
args = parser.parse_args()
|
||||
print('Senpy version {}'.format(senpy.__version__))
|
||||
print(sys.version)
|
||||
if args.version:
|
||||
print('Senpy version {}'.format(senpy.__version__))
|
||||
print(sys.version)
|
||||
exit(1)
|
||||
rl = logging.getLogger()
|
||||
rl.setLevel(getattr(logging, args.level))
|
||||
logger_handler = rl.handlers[0]
|
||||
|
||||
# First, generic formatter:
|
||||
logger_handler.setFormatter(logging.Formatter(args.log_format))
|
||||
|
||||
app = Flask(__name__)
|
||||
app.debug = args.debug
|
||||
sp = Senpy(app, args.plugins_folder,
|
||||
default_plugins=args.default_plugins,
|
||||
sp = Senpy(app,
|
||||
plugin_folder=None,
|
||||
default_plugins=not args.no_default_plugins,
|
||||
data_folder=args.data_folder)
|
||||
folders = list(args.plugins_folder) if args.plugins_folder else []
|
||||
if not folders:
|
||||
folders.append(".")
|
||||
for p in folders:
|
||||
sp.add_folder(p)
|
||||
|
||||
plugins = sp.plugins(plugin_type=None, is_activated=False)
|
||||
maxname = max(len(x.name) for x in plugins)
|
||||
maxversion = max(len(str(x.version)) for x in plugins)
|
||||
print('Found {} plugins:'.format(len(plugins)))
|
||||
for plugin in plugins:
|
||||
import inspect
|
||||
fpath = inspect.getfile(plugin.__class__)
|
||||
print('\t{: <{maxname}} @ {: <{maxversion}} -> {}'.format(plugin.name,
|
||||
plugin.version,
|
||||
fpath,
|
||||
maxname=maxname,
|
||||
maxversion=maxversion))
|
||||
if args.only_list:
|
||||
plugins = sp.plugins()
|
||||
maxname = max(len(x.name) for x in plugins)
|
||||
maxversion = max(len(x.version) for x in plugins)
|
||||
print('Found {} plugins:'.format(len(plugins)))
|
||||
for plugin in plugins:
|
||||
import inspect
|
||||
fpath = inspect.getfile(plugin.__class__)
|
||||
print('\t{: <{maxname}} @ {: <{maxversion}} -> {}'.format(plugin.name,
|
||||
plugin.version,
|
||||
fpath,
|
||||
maxname=maxname,
|
||||
maxversion=maxversion))
|
||||
return
|
||||
if not args.no_deps:
|
||||
sp.install_deps()
|
||||
@ -160,10 +178,13 @@ def main():
|
||||
print('Senpy version {}'.format(senpy.__version__))
|
||||
print('Server running on port %s:%d. Ctrl+C to quit' % (args.host,
|
||||
args.port))
|
||||
app.run(args.host,
|
||||
args.port,
|
||||
threaded=args.threaded,
|
||||
debug=app.debug)
|
||||
try:
|
||||
app.run(args.host,
|
||||
args.port,
|
||||
threaded=not args.no_threaded,
|
||||
debug=app.debug)
|
||||
except KeyboardInterrupt:
|
||||
print('Bye!')
|
||||
sp.deactivate_all()
|
||||
|
||||
|
||||
|
137
senpy/api.py
137
senpy/api.py
@ -5,24 +5,31 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
boolean = [True, False]
|
||||
|
||||
processors = {
|
||||
'string_to_tuple': lambda p: p if isinstance(p, (tuple, list)) else tuple(p.split(','))
|
||||
}
|
||||
|
||||
API_PARAMS = {
|
||||
"algorithm": {
|
||||
"aliases": ["algorithms", "a", "algo"],
|
||||
"required": True,
|
||||
"default": 'default',
|
||||
"processor": 'string_to_tuple',
|
||||
"description": ("Algorithms that will be used to process the request."
|
||||
"It may be a list of comma-separated names."),
|
||||
},
|
||||
"expanded-jsonld": {
|
||||
"@id": "expanded-jsonld",
|
||||
"aliases": ["expanded"],
|
||||
"description": "use JSON-LD expansion to get full URIs",
|
||||
"aliases": ["expanded", "expanded_jsonld"],
|
||||
"options": boolean,
|
||||
"required": True,
|
||||
"default": False
|
||||
},
|
||||
"with_parameters": {
|
||||
"with-parameters": {
|
||||
"aliases": ['withparameters',
|
||||
'with-parameters'],
|
||||
'with_parameters'],
|
||||
"description": "include initial parameters in the response",
|
||||
"options": boolean,
|
||||
"default": False,
|
||||
"required": True
|
||||
@ -31,9 +38,67 @@ API_PARAMS = {
|
||||
"@id": "outformat",
|
||||
"aliases": ["o"],
|
||||
"default": "json-ld",
|
||||
"description": """The data can be semantically formatted (JSON-LD, turtle or n-triples),
|
||||
given as a list of comma-separated fields (see the fields option) or constructed from a Jinja2
|
||||
template (see the template option).""",
|
||||
"required": True,
|
||||
"options": ["json-ld", "turtle", "ntriples"],
|
||||
},
|
||||
"template": {
|
||||
"@id": "template",
|
||||
"required": False,
|
||||
"description": """Jinja2 template for the result. The input data for the template will
|
||||
be the results as a dictionary.
|
||||
For example:
|
||||
|
||||
Consider the results before templating:
|
||||
|
||||
```
|
||||
[{
|
||||
"@type": "entry",
|
||||
"onyx:hasEmotionSet": [],
|
||||
"nif:isString": "testing the template",
|
||||
"marl:hasOpinion": [
|
||||
{
|
||||
"@type": "sentiment",
|
||||
"marl:hasPolarity": "marl:Positive"
|
||||
}
|
||||
]
|
||||
}]
|
||||
```
|
||||
|
||||
|
||||
And the template:
|
||||
|
||||
```
|
||||
{% for entry in entries %}
|
||||
{{ entry["nif:isString"] | upper }},{{entry.sentiments[0]["marl:hasPolarity"].split(":")[1]}}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
The final result would be:
|
||||
|
||||
```
|
||||
TESTING THE TEMPLATE,Positive
|
||||
```
|
||||
"""
|
||||
|
||||
},
|
||||
"fields": {
|
||||
"@id": "fields",
|
||||
"required": False,
|
||||
"description": """A jmespath selector, that can be used to extract a new dictionary, array or value
|
||||
from the results.
|
||||
jmespath is a powerful query language for json and/or dictionaries.
|
||||
It allows you to change the structure (and data) of your objects through queries.
|
||||
|
||||
e.g., the following expression gets a list of `[emotion label, intensity]` for each entry:
|
||||
`entries[]."onyx:hasEmotionSet"[]."onyx:hasEmotion"[]["onyx:hasEmotionCategory","onyx:hasEmotionIntensity"]`
|
||||
|
||||
For more information, see: https://jmespath.org
|
||||
|
||||
"""
|
||||
},
|
||||
"help": {
|
||||
"@id": "help",
|
||||
"description": "Show additional help to know more about the possible parameters",
|
||||
@ -44,21 +109,39 @@ API_PARAMS = {
|
||||
},
|
||||
"verbose": {
|
||||
"@id": "verbose",
|
||||
"description": ("Show all help, including the common API parameters, or "
|
||||
"only plugin-related info"),
|
||||
"description": "Show all properties in the result",
|
||||
"aliases": ["v"],
|
||||
"required": True,
|
||||
"options": boolean,
|
||||
"default": True
|
||||
"default": False
|
||||
},
|
||||
"emotionModel": {
|
||||
"aliases": {
|
||||
"@id": "aliases",
|
||||
"description": "Replace JSON properties with their aliases",
|
||||
"aliases": [],
|
||||
"required": True,
|
||||
"options": boolean,
|
||||
"default": False
|
||||
},
|
||||
"emotion-model": {
|
||||
"@id": "emotionModel",
|
||||
"aliases": ["emoModel"],
|
||||
"description": """Emotion model to use in the response.
|
||||
Senpy will try to convert the output to this model automatically.
|
||||
|
||||
Examples: `wna:liking` and `emoml:big6`.
|
||||
""",
|
||||
"aliases": ["emoModel", "emotionModel"],
|
||||
"required": False
|
||||
},
|
||||
"conversion": {
|
||||
"@id": "conversion",
|
||||
"description": "How to show the elements that have (not) been converted",
|
||||
"description": """How to show the elements that have (not) been converted.
|
||||
|
||||
* full: converted and original elements will appear side-by-side
|
||||
* filtered: only converted elements will be shown
|
||||
* nested: converted elements will be shown, and they will include a link to the original element
|
||||
(using `prov:wasGeneratedBy`).
|
||||
""",
|
||||
"required": True,
|
||||
"options": ["filtered", "nested", "full"],
|
||||
"default": "full"
|
||||
@ -68,9 +151,10 @@ API_PARAMS = {
|
||||
EVAL_PARAMS = {
|
||||
"algorithm": {
|
||||
"aliases": ["plug", "p", "plugins", "algorithms", 'algo', 'a', 'plugin'],
|
||||
"description": "Plugins to be evaluated",
|
||||
"description": "Plugins to evaluate",
|
||||
"required": True,
|
||||
"help": "See activated plugins in /plugins"
|
||||
"help": "See activated plugins in /plugins",
|
||||
"processor": API_PARAMS['algorithm']['processor']
|
||||
},
|
||||
"dataset": {
|
||||
"aliases": ["datasets", "data", "d"],
|
||||
@ -81,18 +165,19 @@ EVAL_PARAMS = {
|
||||
}
|
||||
|
||||
PLUGINS_PARAMS = {
|
||||
"plugin_type": {
|
||||
"plugin-type": {
|
||||
"@id": "pluginType",
|
||||
"description": 'What kind of plugins to list',
|
||||
"aliases": ["pluginType"],
|
||||
"aliases": ["pluginType", "plugin_type"],
|
||||
"required": True,
|
||||
"default": 'analysisPlugin'
|
||||
}
|
||||
}
|
||||
|
||||
WEB_PARAMS = {
|
||||
"inHeaders": {
|
||||
"aliases": ["headers"],
|
||||
"in-headers": {
|
||||
"aliases": ["headers", "inheaders", "inHeaders", "in-headers", "in_headers"],
|
||||
"description": "Only include the JSON-LD context in the headers",
|
||||
"required": True,
|
||||
"default": False,
|
||||
"options": boolean
|
||||
@ -100,8 +185,8 @@ WEB_PARAMS = {
|
||||
}
|
||||
|
||||
CLI_PARAMS = {
|
||||
"plugin_folder": {
|
||||
"aliases": ["folder"],
|
||||
"plugin-folder": {
|
||||
"aliases": ["folder", "plugin_folder"],
|
||||
"required": True,
|
||||
"default": "."
|
||||
},
|
||||
@ -116,6 +201,7 @@ NIF_PARAMS = {
|
||||
},
|
||||
"intype": {
|
||||
"@id": "intype",
|
||||
"description": "input type",
|
||||
"aliases": ["t"],
|
||||
"required": False,
|
||||
"default": "direct",
|
||||
@ -123,6 +209,7 @@ NIF_PARAMS = {
|
||||
},
|
||||
"informat": {
|
||||
"@id": "informat",
|
||||
"description": "input format",
|
||||
"aliases": ["f"],
|
||||
"required": False,
|
||||
"default": "text",
|
||||
@ -130,17 +217,20 @@ NIF_PARAMS = {
|
||||
},
|
||||
"language": {
|
||||
"@id": "language",
|
||||
"description": "language of the input",
|
||||
"aliases": ["l"],
|
||||
"required": False,
|
||||
},
|
||||
"prefix": {
|
||||
"@id": "prefix",
|
||||
"description": "prefix to use for new entities",
|
||||
"aliases": ["p"],
|
||||
"required": True,
|
||||
"default": "",
|
||||
},
|
||||
"urischeme": {
|
||||
"@id": "urischeme",
|
||||
"description": "scheme for NIF URIs",
|
||||
"aliases": ["u"],
|
||||
"required": False,
|
||||
"default": "RFC5147String",
|
||||
@ -171,16 +261,19 @@ def parse_params(indict, *specs):
|
||||
if alias in indict and alias != param:
|
||||
outdict[param] = indict[alias]
|
||||
del outdict[alias]
|
||||
continue
|
||||
break
|
||||
if param not in outdict:
|
||||
if "default" in options:
|
||||
# We assume the default is correct
|
||||
outdict[param] = options["default"]
|
||||
elif options.get("required", False):
|
||||
wrong_params[param] = spec[param]
|
||||
elif "options" in options:
|
||||
continue
|
||||
if 'processor' in options:
|
||||
outdict[param] = processors[options['processor']](outdict[param])
|
||||
if "options" in options:
|
||||
if options["options"] == boolean:
|
||||
outdict[param] = str(outdict[param]).lower() in ['true', '1']
|
||||
outdict[param] = str(outdict[param]).lower() in ['true', '1', '']
|
||||
elif outdict[param] not in options["options"]:
|
||||
wrong_params[param] = spec[param]
|
||||
if wrong_params:
|
||||
@ -253,7 +346,7 @@ def get_extra_params(plugins):
|
||||
return params
|
||||
|
||||
|
||||
def parse_analysis(params, plugins):
|
||||
def parse_analyses(params, plugins):
|
||||
'''
|
||||
Parse the given parameters individually for each plugin, and get a list of the parameters that
|
||||
belong to each of the plugins. Each item can then be used in the plugin.analyse_entries method.
|
||||
@ -305,7 +398,7 @@ def parse_call(params):
|
||||
params = parse_params(params, NIF_PARAMS)
|
||||
if params['informat'] == 'text':
|
||||
results = Results()
|
||||
entry = Entry(nif__isString=params['input'], id='#') # Use @base
|
||||
entry = Entry(nif__isString=params['input'], id='prefix:') # Use @base
|
||||
results.entries.append(entry)
|
||||
elif params['informat'] == 'json-ld':
|
||||
results = from_string(params['input'], cls=Results)
|
||||
|
@ -24,6 +24,8 @@ from . import api
|
||||
from .version import __version__
|
||||
from functools import wraps
|
||||
|
||||
from .gsitk_compat import GSITK_AVAILABLE
|
||||
|
||||
import logging
|
||||
import json
|
||||
import base64
|
||||
@ -63,44 +65,44 @@ def get_params(req):
|
||||
return indict
|
||||
|
||||
|
||||
def encoded_url(url=None, base=None):
|
||||
def encode_url(url=None):
|
||||
code = ''
|
||||
if not url:
|
||||
if request.method == 'GET':
|
||||
url = request.full_path[1:] # Remove the first slash
|
||||
else:
|
||||
hash(frozenset(tuple(request.parameters.items())))
|
||||
code = 'hash:{}'.format(hash)
|
||||
url = request.parameters.get('prefix', request.full_path[1:] + '#')
|
||||
return code or base64.urlsafe_b64encode(url.encode()).decode()
|
||||
|
||||
code = code or base64.urlsafe_b64encode(url.encode()).decode()
|
||||
|
||||
if base:
|
||||
return base + code
|
||||
return url_for('api.decode', code=code, _external=True)
|
||||
def url_for_code(code, base=None):
|
||||
# if base:
|
||||
# return base + code
|
||||
# return url_for('api.decode', code=code, _external=True)
|
||||
# This was producing unique yet very long URIs, which wasn't ideal for visualization.
|
||||
return 'http://senpy.invalid/'
|
||||
|
||||
|
||||
def decoded_url(code, base=None):
|
||||
if code.startswith('hash:'):
|
||||
raise Exception('Can not decode a URL for a POST request')
|
||||
base = base or request.url_root
|
||||
path = base64.urlsafe_b64decode(code.encode()).decode()
|
||||
if path[:4] == 'http':
|
||||
return path
|
||||
base = base or request.url_root
|
||||
return base + path
|
||||
|
||||
|
||||
@demo_blueprint.route('/')
|
||||
def index():
|
||||
ev = str(get_params(request).get('evaluation', False))
|
||||
evaluation_enabled = ev.lower() not in ['false', 'no', 'none']
|
||||
# ev = str(get_params(request).get('evaluation', True))
|
||||
# evaluation_enabled = ev.lower() not in ['false', 'no', 'none']
|
||||
evaluation_enabled = GSITK_AVAILABLE
|
||||
|
||||
return render_template("index.html",
|
||||
evaluation=evaluation_enabled,
|
||||
version=__version__)
|
||||
|
||||
|
||||
@api_blueprint.route('/contexts/<entity>.jsonld')
|
||||
def context(entity="context"):
|
||||
@api_blueprint.route('/contexts/<code>')
|
||||
def context(code=''):
|
||||
context = Response._context
|
||||
context['@vocab'] = url_for('ns.index', _external=True)
|
||||
context['@base'] = url_for('api.decode', code=code, _external=True)
|
||||
context['endpoint'] = url_for('api.api_root', _external=True)
|
||||
return jsonify({"@context": context})
|
||||
|
||||
@ -130,26 +132,59 @@ def schema(schema="definitions"):
|
||||
|
||||
def basic_api(f):
|
||||
default_params = {
|
||||
'inHeaders': False,
|
||||
'in-headers': False,
|
||||
'expanded-jsonld': False,
|
||||
'outformat': None,
|
||||
'with_parameters': True,
|
||||
'with-parameters': True,
|
||||
}
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
raw_params = get_params(request)
|
||||
logger.info('Getting request: {}'.format(raw_params))
|
||||
# logger.info('Getting request: {}'.format(raw_params))
|
||||
logger.debug('Getting request. Params: {}'.format(raw_params))
|
||||
headers = {'X-ORIGINAL-PARAMS': json.dumps(raw_params)}
|
||||
params = default_params
|
||||
|
||||
mime = request.accept_mimetypes\
|
||||
.best_match(MIMETYPES.keys(),
|
||||
DEFAULT_MIMETYPE)
|
||||
|
||||
mimeformat = MIMETYPES.get(mime, DEFAULT_FORMAT)
|
||||
outformat = mimeformat
|
||||
|
||||
try:
|
||||
params = api.parse_params(raw_params, api.WEB_PARAMS, api.API_PARAMS)
|
||||
outformat = params.get('outformat', mimeformat)
|
||||
if hasattr(request, 'parameters'):
|
||||
request.parameters.update(params)
|
||||
else:
|
||||
request.parameters = params
|
||||
response = f(*args, **kwargs)
|
||||
|
||||
if 'parameters' in response and not params['with-parameters']:
|
||||
del response.parameters
|
||||
|
||||
logger.debug('Response: {}'.format(response))
|
||||
|
||||
prefix = params.get('prefix')
|
||||
code = encode_url(prefix)
|
||||
|
||||
return response.flask(
|
||||
in_headers=params['in-headers'],
|
||||
headers=headers,
|
||||
prefix=prefix or url_for_code(code),
|
||||
base=prefix,
|
||||
context_uri=url_for('api.context',
|
||||
code=code,
|
||||
_external=True),
|
||||
outformat=outformat,
|
||||
expanded=params['expanded-jsonld'],
|
||||
template=params.get('template'),
|
||||
verbose=params['verbose'],
|
||||
aliases=params['aliases'],
|
||||
fields=params.get('fields'))
|
||||
|
||||
except (Exception) as ex:
|
||||
if current_app.debug or current_app.config['TESTING']:
|
||||
raise
|
||||
@ -159,56 +194,48 @@ def basic_api(f):
|
||||
response = ex
|
||||
response.parameters = raw_params
|
||||
logger.exception(ex)
|
||||
|
||||
if 'parameters' in response and not params['with_parameters']:
|
||||
del response.parameters
|
||||
|
||||
logger.info('Response: {}'.format(response))
|
||||
mime = request.accept_mimetypes\
|
||||
.best_match(MIMETYPES.keys(),
|
||||
DEFAULT_MIMETYPE)
|
||||
|
||||
mimeformat = MIMETYPES.get(mime, DEFAULT_FORMAT)
|
||||
outformat = params['outformat'] or mimeformat
|
||||
|
||||
return response.flask(
|
||||
in_headers=params['inHeaders'],
|
||||
headers=headers,
|
||||
prefix=params.get('prefix', encoded_url()),
|
||||
context_uri=url_for('api.context',
|
||||
entity=type(response).__name__,
|
||||
_external=True),
|
||||
outformat=outformat,
|
||||
expanded=params['expanded-jsonld'])
|
||||
return response.flask(
|
||||
outformat=outformat,
|
||||
expanded=params['expanded-jsonld'],
|
||||
verbose=params.get('verbose', True),
|
||||
)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
@api_blueprint.route('/', defaults={'plugin': None}, methods=['POST', 'GET'])
|
||||
@api_blueprint.route('/<path:plugin>', methods=['POST', 'GET'])
|
||||
@api_blueprint.route('/', defaults={'plugins': None}, methods=['POST', 'GET'])
|
||||
@api_blueprint.route('/<path:plugins>', methods=['POST', 'GET'])
|
||||
@basic_api
|
||||
def api_root(plugin):
|
||||
if plugin:
|
||||
def api_root(plugins):
|
||||
if plugins:
|
||||
if request.parameters['algorithm'] != api.API_PARAMS['algorithm']['default']:
|
||||
raise Error('You cannot specify the algorithm with a parameter and a URL variable.'
|
||||
' Please, remove one of them')
|
||||
request.parameters['algorithm'] = tuple(plugin.replace('+', '/').split('/'))
|
||||
plugins = plugins.replace('+', ',').replace('/', ',')
|
||||
plugins = api.processors['string_to_tuple'](plugins)
|
||||
else:
|
||||
plugins = request.parameters['algorithm']
|
||||
|
||||
plugin = request.parameters['algorithm']
|
||||
print(plugins)
|
||||
|
||||
sp = current_app.senpy
|
||||
plugins = sp.get_plugins(plugin)
|
||||
plugins = sp.get_plugins(plugins)
|
||||
|
||||
if request.parameters['help']:
|
||||
apis = []
|
||||
if request.parameters['verbose']:
|
||||
apis.append(api.BUILTIN_PARAMS)
|
||||
apis = [api.WEB_PARAMS, api.API_PARAMS, api.NIF_PARAMS]
|
||||
# Verbose is set to False as default, but we want it to default to
|
||||
# True for help. This checks the original value, to make sure it wasn't
|
||||
# set by default.
|
||||
if not request.parameters['verbose'] and get_params(request).get('verbose'):
|
||||
apis = []
|
||||
if request.parameters['algorithm'] == ['default', ]:
|
||||
plugins = []
|
||||
allparameters = api.get_all_params(plugins, *apis)
|
||||
response = Help(valid_parameters=allparameters)
|
||||
return response
|
||||
req = api.parse_call(request.parameters)
|
||||
analysis = api.parse_analysis(req.parameters, plugins)
|
||||
results = current_app.senpy.analyse(req, analysis)
|
||||
analyses = api.parse_analyses(req.parameters, plugins)
|
||||
results = current_app.senpy.analyse(req, analyses)
|
||||
return results
|
||||
|
||||
|
||||
@ -230,8 +257,8 @@ def evaluate():
|
||||
def plugins():
|
||||
sp = current_app.senpy
|
||||
params = api.parse_params(request.parameters, api.PLUGINS_PARAMS)
|
||||
ptype = params.get('plugin_type')
|
||||
plugins = list(sp.plugins(plugin_type=ptype))
|
||||
ptype = params.get('plugin-type')
|
||||
plugins = list(sp.analysis_plugins(plugin_type=ptype))
|
||||
dic = Plugins(plugins=plugins)
|
||||
return dic
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
from .models import Error
|
||||
from .extensions import Senpy
|
||||
@ -27,8 +29,8 @@ def main_function(argv):
|
||||
api.CLI_PARAMS,
|
||||
api.API_PARAMS,
|
||||
api.NIF_PARAMS)
|
||||
plugin_folder = params['plugin_folder']
|
||||
default_plugins = params.get('default-plugins', False)
|
||||
plugin_folder = params['plugin-folder']
|
||||
default_plugins = not params.get('no-default-plugins', False)
|
||||
sp = Senpy(default_plugins=default_plugins, plugin_folder=plugin_folder)
|
||||
request = api.parse_call(params)
|
||||
algos = sp.get_plugins(request.parameters.get('algorithm', None))
|
||||
@ -48,7 +50,7 @@ def main():
|
||||
res = main_function(sys.argv[1:])
|
||||
print(res.serialize())
|
||||
except Error as err:
|
||||
print(err.serialize())
|
||||
print(err.serialize(), file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
|
@ -7,6 +7,7 @@ standard_library.install_aliases()
|
||||
|
||||
from . import plugins, api
|
||||
from .models import Error, AggregatedEvaluation
|
||||
from .plugins import AnalysisPlugin
|
||||
from .blueprints import api_blueprint, demo_blueprint, ns_blueprint
|
||||
|
||||
from threading import Thread
|
||||
@ -54,6 +55,7 @@ class Senpy(object):
|
||||
self.app = app
|
||||
if app is not None:
|
||||
self.init_app(app)
|
||||
self._conversion_candidates = {}
|
||||
|
||||
def init_app(self, app):
|
||||
""" Initialise a flask app to add plugins to its context """
|
||||
@ -74,14 +76,18 @@ class Senpy(object):
|
||||
|
||||
def add_plugin(self, plugin):
|
||||
self._plugins[plugin.name.lower()] = plugin
|
||||
self._conversion_candidates = {}
|
||||
|
||||
def delete_plugin(self, plugin):
|
||||
del self._plugins[plugin.name.lower()]
|
||||
|
||||
def plugins(self, plugin_type=None, is_activated=True, **kwargs):
|
||||
""" Return the plugins registered for a given application. Filtered by criteria """
|
||||
return list(plugins.pfilter(self._plugins, plugin_type=plugin_type,
|
||||
is_activated=is_activated, **kwargs))
|
||||
return sorted(plugins.pfilter(self._plugins,
|
||||
plugin_type=plugin_type,
|
||||
is_activated=is_activated,
|
||||
**kwargs),
|
||||
key=lambda x: x.id)
|
||||
|
||||
def get_plugin(self, name, default=None):
|
||||
if name == 'default':
|
||||
@ -115,10 +121,10 @@ class Senpy(object):
|
||||
pass # Assume it is a tuple or a list
|
||||
return tuple(self.get_plugin(n) for n in name)
|
||||
|
||||
@property
|
||||
def analysis_plugins(self):
|
||||
def analysis_plugins(self, **kwargs):
|
||||
""" Return only the analysis plugins that are active"""
|
||||
return self.plugins(plugin_type='analysisPlugin', is_activated=True)
|
||||
candidates = self.plugins(**kwargs)
|
||||
return list(plugins.pfilter(candidates, plugin_type=AnalysisPlugin))
|
||||
|
||||
def add_folder(self, folder, from_root=False):
|
||||
""" Find plugins in this folder and add them to this instance """
|
||||
@ -144,14 +150,17 @@ class Senpy(object):
|
||||
|
||||
analysis = pending[0]
|
||||
results = analysis.run(req)
|
||||
results.analysis.append(analysis)
|
||||
results.activities.append(analysis)
|
||||
done += analysis
|
||||
return self._process(results, pending[1:], done)
|
||||
|
||||
def install_deps(self):
|
||||
plugins.install_deps(*self.plugins())
|
||||
logger.info('Installing dependencies')
|
||||
# If a plugin is activated, its dependencies should already be installed
|
||||
# Otherwise, it would've failed to activate.
|
||||
plugins.install_deps(*self.plugins(is_activated=False))
|
||||
|
||||
def analyse(self, request, analysis=None):
|
||||
def analyse(self, request, analyses=None):
|
||||
"""
|
||||
Main method that analyses a request, either from CLI or HTTP.
|
||||
It takes a processed request, provided by the user, as returned
|
||||
@ -162,17 +171,17 @@ class Senpy(object):
|
||||
status=404,
|
||||
message=("No plugins found."
|
||||
" Please install one."))
|
||||
if analysis is None:
|
||||
if analyses is None:
|
||||
plugins = self.get_plugins(request.parameters['algorithm'])
|
||||
analysis = api.parse_analysis(request.parameters, plugins)
|
||||
analyses = api.parse_analyses(request.parameters, plugins)
|
||||
logger.debug("analysing request: {}".format(request))
|
||||
results = self._process(request, analysis)
|
||||
results = self._process(request, analyses)
|
||||
logger.debug("Got analysis result: {}".format(results))
|
||||
results = self.postprocess(results)
|
||||
results = self.postprocess(results, analyses)
|
||||
logger.debug("Returning post-processed result: {}".format(results))
|
||||
return results
|
||||
|
||||
def convert_emotions(self, resp):
|
||||
def convert_emotions(self, resp, analyses):
|
||||
"""
|
||||
Conversion of all emotions in a response **in place**.
|
||||
In addition to converting from one model to another, it has
|
||||
@ -180,45 +189,50 @@ class Senpy(object):
|
||||
Needless to say, this is far from an elegant solution, but it works.
|
||||
@todo refactor and clean up
|
||||
"""
|
||||
plugins = resp.analysis
|
||||
|
||||
logger.debug("Converting emotions")
|
||||
if 'parameters' not in resp:
|
||||
logger.debug("NO PARAMETERS")
|
||||
return resp
|
||||
|
||||
params = resp['parameters']
|
||||
toModel = params.get('emotionModel', None)
|
||||
toModel = params.get('emotion-model', None)
|
||||
if not toModel:
|
||||
logger.debug("NO tomodel PARAMETER")
|
||||
return resp
|
||||
|
||||
logger.debug('Asked for model: {}'.format(toModel))
|
||||
output = params.get('conversion', None)
|
||||
candidates = {}
|
||||
for plugin in plugins:
|
||||
try:
|
||||
fromModel = plugin.get('onyx:usesEmotionModel', None)
|
||||
candidates[plugin.id] = next(self._conversion_candidates(fromModel, toModel))
|
||||
logger.debug('Analysis plugin {} uses model: {}'.format(
|
||||
plugin.id, fromModel))
|
||||
except StopIteration:
|
||||
e = Error(('No conversion plugin found for: '
|
||||
'{} -> {}'.format(fromModel, toModel)),
|
||||
status=404)
|
||||
e.original_response = resp
|
||||
e.parameters = params
|
||||
raise e
|
||||
|
||||
newentries = []
|
||||
done = []
|
||||
for i in resp.entries:
|
||||
|
||||
if output == "full":
|
||||
newemotions = copy.deepcopy(i.emotions)
|
||||
else:
|
||||
newemotions = []
|
||||
for j in i.emotions:
|
||||
plugname = j['prov:wasGeneratedBy']
|
||||
candidate = candidates[plugname]
|
||||
done.append({'plugin': candidate, 'parameters': params})
|
||||
activity = j['prov:wasGeneratedBy']
|
||||
act = resp.activity(activity)
|
||||
if not act:
|
||||
raise Error('Could not find the emotion model for {}'.format(activity))
|
||||
fromModel = act.plugin['onyx:usesEmotionModel']
|
||||
if toModel == fromModel:
|
||||
continue
|
||||
candidate = self._conversion_candidate(fromModel, toModel)
|
||||
if not candidate:
|
||||
e = Error(('No conversion plugin found for: '
|
||||
'{} -> {}'.format(fromModel, toModel)),
|
||||
status=404)
|
||||
e.original_response = resp
|
||||
e.parameters = params
|
||||
raise e
|
||||
|
||||
analysis = candidate.activity(params)
|
||||
done.append(analysis)
|
||||
for k in candidate.convert(j, fromModel, toModel, params):
|
||||
k.prov__wasGeneratedBy = candidate.id
|
||||
k.prov__wasGeneratedBy = analysis.id
|
||||
if output == 'nested':
|
||||
k.prov__wasDerivedFrom = j
|
||||
newemotions.append(k)
|
||||
@ -227,26 +241,36 @@ class Senpy(object):
|
||||
resp.entries = newentries
|
||||
return resp
|
||||
|
||||
def _conversion_candidates(self, fromModel, toModel):
|
||||
candidates = self.plugins(plugin_type=plugins.EmotionConversion)
|
||||
for candidate in candidates:
|
||||
for pair in candidate.onyx__doesConversion:
|
||||
logging.debug(pair)
|
||||
if candidate.can_convert(fromModel, toModel):
|
||||
yield candidate
|
||||
def _conversion_candidate(self, fromModel, toModel):
|
||||
if not self._conversion_candidates:
|
||||
candidates = {}
|
||||
for conv in self.plugins(plugin_type=plugins.EmotionConversion):
|
||||
for pair in conv.onyx__doesConversion:
|
||||
logging.debug(pair)
|
||||
key = (pair['onyx:conversionFrom'], pair['onyx:conversionTo'])
|
||||
if key not in candidates:
|
||||
candidates[key] = []
|
||||
candidates[key].append(conv)
|
||||
self._conversion_candidates = candidates
|
||||
|
||||
def postprocess(self, response):
|
||||
key = (fromModel, toModel)
|
||||
if key not in self._conversion_candidates:
|
||||
return None
|
||||
return self._conversion_candidates[key][0]
|
||||
|
||||
def postprocess(self, response, analyses):
|
||||
'''
|
||||
Transform the results from the analysis plugins.
|
||||
It has some pre-defined post-processing like emotion conversion,
|
||||
and it also allows plugins to auto-select themselves.
|
||||
'''
|
||||
|
||||
response = self.convert_emotions(response)
|
||||
response = self.convert_emotions(response, analyses)
|
||||
|
||||
for plug in self.plugins(plugin_type=plugins.PostProcessing):
|
||||
if plug.check(response, response.analysis):
|
||||
response = plug.process(response)
|
||||
if plug.check(response, response.activities):
|
||||
activity = plug.activity(response.parameters)
|
||||
response = plug.process(response, activity)
|
||||
return response
|
||||
|
||||
def _get_datasets(self, request):
|
||||
@ -286,13 +310,16 @@ class Senpy(object):
|
||||
results = AggregatedEvaluation()
|
||||
results.parameters = params
|
||||
datasets = self._get_datasets(results)
|
||||
plugins = []
|
||||
for plugname in params.algorithm:
|
||||
plugins = self.get_plugin(plugname)
|
||||
plugs = []
|
||||
for plugname in params['algorithm']:
|
||||
plugs = self.get_plugins(plugname)
|
||||
for plug in plugs:
|
||||
if not isinstance(plug, plugins.Evaluable):
|
||||
raise Exception('Plugin {} can not be evaluated', plug.id)
|
||||
|
||||
for eval in plugins.evaluate(plugins, datasets):
|
||||
for eval in plugins.evaluate(plugs, datasets):
|
||||
results.evaluations.append(eval)
|
||||
if 'with_parameters' not in results.parameters:
|
||||
if 'with-parameters' not in results.parameters:
|
||||
del results.parameters
|
||||
logger.debug("Returning evaluation result: {}".format(results))
|
||||
return results
|
||||
@ -300,8 +327,7 @@ class Senpy(object):
|
||||
@property
|
||||
def default_plugin(self):
|
||||
if not self._default or not self._default.is_activated:
|
||||
candidates = self.plugins(
|
||||
plugin_type='analysisPlugin', is_activated=True)
|
||||
candidates = self.analysis_plugins()
|
||||
if len(candidates) > 0:
|
||||
self._default = candidates[0]
|
||||
else:
|
||||
@ -336,22 +362,15 @@ class Senpy(object):
|
||||
ps.append(self.deactivate_plugin(plug, sync=sync))
|
||||
return ps
|
||||
|
||||
def _set_active(self, plugin, active=True, *args, **kwargs):
|
||||
''' We're using a variable in the plugin itself to activate/deactivate plugins.\
|
||||
Note that plugins may activate themselves by setting this variable.
|
||||
'''
|
||||
plugin.is_activated = active
|
||||
|
||||
def _activate(self, plugin):
|
||||
success = False
|
||||
with plugin._lock:
|
||||
if plugin.is_activated:
|
||||
return
|
||||
plugin.activate()
|
||||
plugin._activate()
|
||||
msg = "Plugin activated: {}".format(plugin.name)
|
||||
logger.info(msg)
|
||||
success = True
|
||||
self._set_active(plugin, success)
|
||||
success = plugin.is_activated
|
||||
return success
|
||||
|
||||
def activate_plugin(self, plugin_name, sync=True):
|
||||
@ -375,7 +394,7 @@ class Senpy(object):
|
||||
with plugin._lock:
|
||||
if not plugin.is_activated:
|
||||
return
|
||||
plugin.deactivate()
|
||||
plugin._deactivate()
|
||||
logger.info("Plugin deactivated: {}".format(plugin.name))
|
||||
|
||||
def deactivate_plugin(self, plugin_name, sync=True):
|
||||
@ -385,13 +404,11 @@ class Senpy(object):
|
||||
message="Plugin not found: {}".format(plugin_name), status=404)
|
||||
plugin = self._plugins[plugin_name]
|
||||
|
||||
self._set_active(plugin, False)
|
||||
|
||||
if sync or not getattr(plugin, 'async', True) or not getattr(
|
||||
plugin, 'sync', False):
|
||||
self._deactivate(plugin)
|
||||
plugin._deactivate()
|
||||
else:
|
||||
th = Thread(target=partial(self._deactivate, plugin))
|
||||
th = Thread(target=plugin.deactivate)
|
||||
th.start()
|
||||
return th
|
||||
|
||||
|
@ -16,16 +16,16 @@ def raise_exception(*args, **kwargs):
|
||||
try:
|
||||
gsitk_distro = get_distribution("gsitk")
|
||||
GSITK_VERSION = parse_version(gsitk_distro.version)
|
||||
GSITK_AVAILABLE = GSITK_VERSION > parse_version("0.1.9.1") # Earlier versions have a bug
|
||||
except DistributionNotFound:
|
||||
GSITK_AVAILABLE = False
|
||||
GSITK_VERSION = ()
|
||||
|
||||
if GSITK_AVAILABLE:
|
||||
from gsitk.datasets.datasets import DatasetManager
|
||||
from gsitk.evaluation.evaluation import Evaluation as Eval
|
||||
from gsitk.evaluation.evaluation import Evaluation as Eval # noqa: F401
|
||||
from gsitk.evaluation.evaluation import EvalPipeline # noqa: F401
|
||||
from sklearn.pipeline import Pipeline
|
||||
modules = locals()
|
||||
else:
|
||||
GSITK_AVAILABLE = True
|
||||
except (DistributionNotFound, ImportError) as err:
|
||||
logger.debug('Error importing GSITK: {}'.format(err))
|
||||
logger.warning(IMPORTMSG)
|
||||
GSITK_AVAILABLE = False
|
||||
GSITK_VERSION = ()
|
||||
DatasetManager = Eval = Pipeline = raise_exception
|
||||
|
@ -34,6 +34,7 @@ class BaseMeta(ABCMeta):
|
||||
def __new__(mcs, name, bases, attrs, **kwargs):
|
||||
register_afterwards = False
|
||||
defaults = {}
|
||||
aliases = {}
|
||||
|
||||
attrs = mcs.expand_with_schema(name, attrs)
|
||||
if 'schema' in attrs:
|
||||
@ -41,17 +42,21 @@ class BaseMeta(ABCMeta):
|
||||
for base in bases:
|
||||
if hasattr(base, '_defaults'):
|
||||
defaults.update(getattr(base, '_defaults'))
|
||||
if hasattr(base, '_aliases'):
|
||||
aliases.update(getattr(base, '_aliases'))
|
||||
|
||||
info, rest = mcs.split_attrs(attrs)
|
||||
|
||||
for i in list(info.keys()):
|
||||
if isinstance(info[i], _Alias):
|
||||
fget, fset, fdel = make_property(info[i].indict)
|
||||
rest[i] = property(fget=fget, fset=fset, fdel=fdel)
|
||||
aliases[i] = info[i].indict
|
||||
if info[i].default is not None:
|
||||
defaults[i] = info[i].default
|
||||
else:
|
||||
defaults[i] = info[i]
|
||||
|
||||
rest['_defaults'] = defaults
|
||||
rest['_aliases'] = aliases
|
||||
|
||||
cls = super(BaseMeta, mcs).__new__(mcs, name, tuple(bases), rest)
|
||||
|
||||
@ -86,7 +91,7 @@ class BaseMeta(ABCMeta):
|
||||
|
||||
resolver = jsonschema.RefResolver(schema_path, schema)
|
||||
if '@type' not in attrs:
|
||||
attrs['@type'] = "".join((name[0].lower(), name[1:]))
|
||||
attrs['@type'] = name
|
||||
attrs['_schema_file'] = schema_file
|
||||
attrs['schema'] = schema
|
||||
attrs['_validator'] = jsonschema.Draft4Validator(schema, resolver=resolver)
|
||||
@ -140,9 +145,11 @@ class BaseMeta(ABCMeta):
|
||||
return temp
|
||||
|
||||
|
||||
def make_property(key):
|
||||
def make_property(key, default=None):
|
||||
|
||||
def fget(self):
|
||||
if default:
|
||||
return self.get(key, copy.copy(default))
|
||||
return self[key]
|
||||
|
||||
def fdel(self):
|
||||
@ -168,7 +175,7 @@ class CustomDict(MutableMapping, object):
|
||||
'''
|
||||
|
||||
_defaults = {}
|
||||
_map_attr_key = {'id': '@id'}
|
||||
_aliases = {'id': '@id'}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CustomDict, self).__init__()
|
||||
@ -177,13 +184,13 @@ class CustomDict(MutableMapping, object):
|
||||
for arg in args:
|
||||
self.update(arg)
|
||||
for k, v in kwargs.items():
|
||||
self[self._attr_to_key(k)] = v
|
||||
self[k] = v
|
||||
return self
|
||||
|
||||
def serializable(self):
|
||||
def serializable(self, **kwargs):
|
||||
def ser_or_down(item):
|
||||
if hasattr(item, 'serializable'):
|
||||
return item.serializable()
|
||||
return item.serializable(**kwargs)
|
||||
elif isinstance(item, dict):
|
||||
temp = dict()
|
||||
for kp in item:
|
||||
@ -195,10 +202,9 @@ class CustomDict(MutableMapping, object):
|
||||
else:
|
||||
return item
|
||||
|
||||
return ser_or_down(self.as_dict())
|
||||
return ser_or_down(self.as_dict(**kwargs))
|
||||
|
||||
def __getitem__(self, key):
|
||||
key = self._key_to_attr(key)
|
||||
return self.__dict__[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
@ -206,9 +212,23 @@ class CustomDict(MutableMapping, object):
|
||||
key = self._key_to_attr(key)
|
||||
return setattr(self, key, value)
|
||||
|
||||
def as_dict(self):
|
||||
return {self._attr_to_key(k): v for k, v in self.__dict__.items()
|
||||
if not self._internal_key(k)}
|
||||
def __delitem__(self, key):
|
||||
key = self._key_to_attr(key)
|
||||
del self.__dict__[key]
|
||||
|
||||
def as_dict(self, verbose=True, aliases=False):
|
||||
attrs = self.__dict__.keys()
|
||||
if not verbose and hasattr(self, '_terse_keys'):
|
||||
attrs = self._terse_keys + ['@type', '@id']
|
||||
res = {k: getattr(self, k) for k in attrs
|
||||
if not self._internal_key(k) and hasattr(self, k)}
|
||||
if not aliases:
|
||||
return res
|
||||
for k, ok in self._aliases.items():
|
||||
if ok in res:
|
||||
res[k] = getattr(res, ok)
|
||||
del res[ok]
|
||||
return res
|
||||
|
||||
def __iter__(self):
|
||||
return (k for k in self.__dict__ if not self._internal_key(k))
|
||||
@ -216,29 +236,38 @@ class CustomDict(MutableMapping, object):
|
||||
def __len__(self):
|
||||
return len(self.__dict__)
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self.__dict__[key]
|
||||
|
||||
def update(self, other):
|
||||
for k, v in other.items():
|
||||
self[k] = v
|
||||
|
||||
def _attr_to_key(self, key):
|
||||
key = key.replace("__", ":", 1)
|
||||
key = self._map_attr_key.get(key, key)
|
||||
key = self._aliases.get(key, key)
|
||||
return key
|
||||
|
||||
def _key_to_attr(self, key):
|
||||
if self._internal_key(key):
|
||||
return key
|
||||
key = key.replace(":", "__", 1)
|
||||
|
||||
if key in self._aliases:
|
||||
key = self._aliases[key]
|
||||
else:
|
||||
key = key.replace(":", "__", 1)
|
||||
return key
|
||||
|
||||
def __getattr__(self, key):
|
||||
try:
|
||||
return self.__dict__[self._attr_to_key(key)]
|
||||
except KeyError:
|
||||
raise AttributeError
|
||||
nkey = self._attr_to_key(key)
|
||||
if nkey in self.__dict__:
|
||||
return self.__dict__[nkey]
|
||||
elif nkey == key:
|
||||
raise AttributeError("Key not found: {}".format(key))
|
||||
return getattr(self, nkey)
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
super(CustomDict, self).__setattr__(self._attr_to_key(key), value)
|
||||
|
||||
def __delattr__(self, key):
|
||||
super(CustomDict, self).__delattr__(self._attr_to_key(key))
|
||||
|
||||
@staticmethod
|
||||
def _internal_key(key):
|
||||
@ -251,8 +280,8 @@ class CustomDict(MutableMapping, object):
|
||||
return json.dumps(self.serializable(), sort_keys=True, indent=4)
|
||||
|
||||
|
||||
_Alias = namedtuple('Alias', 'indict')
|
||||
_Alias = namedtuple('Alias', ['indict', 'default'])
|
||||
|
||||
|
||||
def alias(key):
|
||||
return _Alias(key)
|
||||
def alias(key, default=None):
|
||||
return _Alias(key, default)
|
||||
|
259
senpy/models.py
259
senpy/models.py
@ -12,6 +12,8 @@ standard_library.install_aliases()
|
||||
from future.utils import with_metaclass
|
||||
from past.builtins import basestring
|
||||
|
||||
from jinja2 import Environment, BaseLoader
|
||||
|
||||
import time
|
||||
import copy
|
||||
import json
|
||||
@ -21,6 +23,7 @@ from flask import Response as FlaskResponse
|
||||
from pyld import jsonld
|
||||
|
||||
import logging
|
||||
import jmespath
|
||||
|
||||
logging.getLogger('rdflib').setLevel(logging.WARN)
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -31,8 +34,9 @@ from rdflib import Graph
|
||||
from .meta import BaseMeta, CustomDict, alias
|
||||
|
||||
DEFINITIONS_FILE = 'definitions.json'
|
||||
CONTEXT_PATH = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)), 'schemas', 'context.jsonld')
|
||||
CONTEXT_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
||||
'schemas',
|
||||
'context.jsonld')
|
||||
|
||||
|
||||
def get_schema_path(schema_file, absolute=False):
|
||||
@ -132,13 +136,10 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
||||
if auto_id:
|
||||
self.id
|
||||
|
||||
if '@type' not in self:
|
||||
logger.warning('Created an instance of an unknown model')
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
if '@id' not in self:
|
||||
self['@id'] = '_:{}_{}'.format(type(self).__name__, time.time())
|
||||
self['@id'] = 'prefix:{}_{}'.format(type(self).__name__, time.time())
|
||||
return self['@id']
|
||||
|
||||
@id.setter
|
||||
@ -174,24 +175,33 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
||||
headers=headers,
|
||||
mimetype=mimetype)
|
||||
|
||||
def serialize(self, format='json-ld', with_mime=False, **kwargs):
|
||||
js = self.jsonld(**kwargs)
|
||||
content = json.dumps(js, indent=2, sort_keys=True)
|
||||
if format == 'json-ld':
|
||||
def serialize(self, format='json-ld', with_mime=False,
|
||||
template=None, prefix=None, fields=None, **kwargs):
|
||||
js = self.jsonld(prefix=prefix, **kwargs)
|
||||
if template is not None:
|
||||
rtemplate = Environment(loader=BaseLoader).from_string(template)
|
||||
content = rtemplate.render(**self)
|
||||
mimetype = 'text'
|
||||
elif fields is not None:
|
||||
# Emulate field selection by constructing a template
|
||||
content = json.dumps(jmespath.search(fields, js))
|
||||
mimetype = 'text'
|
||||
elif format == 'json-ld':
|
||||
content = json.dumps(js, indent=2, sort_keys=True)
|
||||
mimetype = "application/json"
|
||||
elif format in ['turtle', 'ntriples']:
|
||||
content = json.dumps(js, indent=2, sort_keys=True)
|
||||
logger.debug(js)
|
||||
base = kwargs.get('prefix')
|
||||
context = [self._context, {'prefix': prefix, '@base': prefix}]
|
||||
g = Graph().parse(
|
||||
data=content,
|
||||
format='json-ld',
|
||||
base=base,
|
||||
context=[self._context,
|
||||
{'@base': base}])
|
||||
prefix=prefix,
|
||||
context=context)
|
||||
logger.debug(
|
||||
'Parsing with prefix: {}'.format(kwargs.get('prefix')))
|
||||
content = g.serialize(format=format,
|
||||
base=base).decode('utf-8')
|
||||
prefix=prefix).decode('utf-8')
|
||||
mimetype = 'text/{}'.format(format)
|
||||
else:
|
||||
raise Error('Unknown outformat: {}'.format(format))
|
||||
@ -204,14 +214,25 @@ class BaseModel(with_metaclass(BaseMeta, CustomDict)):
|
||||
with_context=False,
|
||||
context_uri=None,
|
||||
prefix=None,
|
||||
expanded=False):
|
||||
base=None,
|
||||
expanded=False,
|
||||
**kwargs):
|
||||
|
||||
result = self.serializable()
|
||||
result = self.serializable(**kwargs)
|
||||
|
||||
if expanded:
|
||||
result = jsonld.expand(
|
||||
result, options={'base': prefix,
|
||||
'expandContext': self._context})[0]
|
||||
result,
|
||||
options={
|
||||
'expandContext': [
|
||||
self._context,
|
||||
{
|
||||
'prefix': prefix,
|
||||
'endpoint': prefix
|
||||
}
|
||||
]
|
||||
}
|
||||
)[0]
|
||||
if not with_context:
|
||||
try:
|
||||
del result['@context']
|
||||
@ -239,7 +260,7 @@ def subtypes():
|
||||
return BaseMeta._subtypes
|
||||
|
||||
|
||||
def from_dict(indict, cls=None):
|
||||
def from_dict(indict, cls=None, warn=True):
|
||||
if not cls:
|
||||
target = indict.get('@type', None)
|
||||
cls = BaseModel
|
||||
@ -247,6 +268,10 @@ def from_dict(indict, cls=None):
|
||||
cls = subtypes()[target]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if cls == BaseModel and warn:
|
||||
logger.warning('Created an instance of an unknown model')
|
||||
|
||||
outdict = dict()
|
||||
for k, v in indict.items():
|
||||
if k == '@context':
|
||||
@ -266,22 +291,24 @@ def from_string(string, **kwargs):
|
||||
return from_dict(json.loads(string), **kwargs)
|
||||
|
||||
|
||||
def from_json(injson):
|
||||
def from_json(injson, **kwargs):
|
||||
indict = json.loads(injson)
|
||||
return from_dict(indict)
|
||||
return from_dict(indict, **kwargs)
|
||||
|
||||
|
||||
class Entry(BaseModel):
|
||||
schema = 'entry'
|
||||
|
||||
text = alias('nif:isString')
|
||||
sentiments = alias('marl:hasOpinion', [])
|
||||
emotions = alias('onyx:hasEmotionSet', [])
|
||||
|
||||
|
||||
class Sentiment(BaseModel):
|
||||
schema = 'sentiment'
|
||||
|
||||
polarity = alias('marl:hasPolarity')
|
||||
polarityValue = alias('marl:hasPolarityValue')
|
||||
polarityValue = alias('marl:polarityValue')
|
||||
|
||||
|
||||
class Error(BaseModel, Exception):
|
||||
@ -301,59 +328,121 @@ class Error(BaseModel, Exception):
|
||||
return Exception.__hash__(self)
|
||||
|
||||
|
||||
# Add the remaining schemas programmatically
|
||||
class AggregatedEvaluation(BaseModel):
|
||||
schema = 'aggregatedEvaluation'
|
||||
|
||||
def _class_from_schema(name, schema=None, schema_file=None, base_classes=None):
|
||||
base_classes = base_classes or []
|
||||
base_classes.append(BaseModel)
|
||||
attrs = {}
|
||||
if schema:
|
||||
attrs['schema'] = schema
|
||||
elif schema_file:
|
||||
attrs['schema_file'] = schema_file
|
||||
else:
|
||||
attrs['schema'] = name
|
||||
name = "".join((name[0].upper(), name[1:]))
|
||||
return BaseMeta(name, base_classes, attrs)
|
||||
evaluations = alias('senpy:evaluations', [])
|
||||
|
||||
|
||||
def _add_class_from_schema(*args, **kwargs):
|
||||
generatedClass = _class_from_schema(*args, **kwargs)
|
||||
globals()[generatedClass.__name__] = generatedClass
|
||||
del generatedClass
|
||||
class Dataset(BaseModel):
|
||||
schema = 'dataset'
|
||||
|
||||
|
||||
for i in [
|
||||
'aggregatedEvaluation',
|
||||
'dataset',
|
||||
'datasets',
|
||||
'emotion',
|
||||
'emotionConversion',
|
||||
'emotionConversionPlugin',
|
||||
'emotionAnalysis',
|
||||
'emotionModel',
|
||||
'emotionPlugin',
|
||||
'emotionSet',
|
||||
'evaluation',
|
||||
'entity',
|
||||
'help',
|
||||
'metric',
|
||||
'parameter',
|
||||
'plugins',
|
||||
'response',
|
||||
'results',
|
||||
'sentimentPlugin',
|
||||
'suggestion',
|
||||
'topic',
|
||||
class Datasets(BaseModel):
|
||||
schema = 'datasets'
|
||||
|
||||
]:
|
||||
_add_class_from_schema(i)
|
||||
datasets = []
|
||||
|
||||
|
||||
class Emotion(BaseModel):
|
||||
schema = 'emotion'
|
||||
|
||||
|
||||
class EmotionConversion(BaseModel):
|
||||
schema = 'emotionConversion'
|
||||
|
||||
|
||||
class EmotionConversionPlugin(BaseModel):
|
||||
schema = 'emotionConversionPlugin'
|
||||
|
||||
|
||||
class EmotionAnalysis(BaseModel):
|
||||
schema = 'emotionAnalysis'
|
||||
|
||||
|
||||
class EmotionModel(BaseModel):
|
||||
schema = 'emotionModel'
|
||||
onyx__hasEmotionCategory = []
|
||||
|
||||
|
||||
class EmotionPlugin(BaseModel):
|
||||
schema = 'emotionPlugin'
|
||||
|
||||
|
||||
class EmotionSet(BaseModel):
|
||||
schema = 'emotionSet'
|
||||
|
||||
onyx__hasEmotion = []
|
||||
|
||||
|
||||
class Evaluation(BaseModel):
|
||||
schema = 'evaluation'
|
||||
|
||||
metrics = alias('senpy:metrics', [])
|
||||
|
||||
|
||||
class Entity(BaseModel):
|
||||
schema = 'entity'
|
||||
|
||||
|
||||
class Help(BaseModel):
|
||||
schema = 'help'
|
||||
|
||||
|
||||
class Metric(BaseModel):
|
||||
schema = 'metric'
|
||||
|
||||
|
||||
class Parameter(BaseModel):
|
||||
schema = 'parameter'
|
||||
|
||||
|
||||
class Plugins(BaseModel):
|
||||
schema = 'plugins'
|
||||
|
||||
plugins = []
|
||||
|
||||
|
||||
class Response(BaseModel):
|
||||
schema = 'response'
|
||||
|
||||
|
||||
class Results(BaseModel):
|
||||
schema = 'results'
|
||||
|
||||
_terse_keys = ['entries', ]
|
||||
|
||||
activities = []
|
||||
entries = []
|
||||
|
||||
def activity(self, id):
|
||||
for i in self.activities:
|
||||
if i.id == id:
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
class SentimentPlugin(BaseModel):
|
||||
schema = 'sentimentPlugin'
|
||||
|
||||
|
||||
class Suggestion(BaseModel):
|
||||
schema = 'suggestion'
|
||||
|
||||
|
||||
class Topic(BaseModel):
|
||||
schema = 'topic'
|
||||
|
||||
|
||||
class Analysis(BaseModel):
|
||||
'''
|
||||
A prov:Activity that results of executing a Plugin on an entry with a set of
|
||||
parameters.
|
||||
'''
|
||||
schema = 'analysis'
|
||||
|
||||
parameters = alias('prov:used')
|
||||
parameters = alias('prov:used', [])
|
||||
algorithm = alias('prov:wasAssociatedWith', [])
|
||||
|
||||
@property
|
||||
def params(self):
|
||||
@ -373,9 +462,11 @@ class Analysis(BaseModel):
|
||||
else:
|
||||
self.parameters.append(Parameter(name=k, value=v)) # noqa: F821
|
||||
|
||||
@property
|
||||
def algorithm(self):
|
||||
return self['prov:wasAssociatedWith']
|
||||
def param(self, key, default=None):
|
||||
for param in self.parameters:
|
||||
if param['name'] == key:
|
||||
return param['value']
|
||||
return default
|
||||
|
||||
@property
|
||||
def plugin(self):
|
||||
@ -387,15 +478,39 @@ class Analysis(BaseModel):
|
||||
self['prov:wasAssociatedWith'] = value.id
|
||||
|
||||
def run(self, request):
|
||||
return self.plugin.process(request, self.params)
|
||||
return self.plugin.process(request, self)
|
||||
|
||||
|
||||
class Plugin(BaseModel):
|
||||
schema = 'plugin'
|
||||
extra_params = {}
|
||||
|
||||
def activity(self, parameters):
|
||||
'''Generate a prov:Activity from this plugin and the '''
|
||||
def activity(self, parameters=None):
|
||||
'''Generate an Analysis (prov:Activity) from this plugin and the given parameters'''
|
||||
a = Analysis()
|
||||
a.plugin = self
|
||||
a.params = parameters
|
||||
if parameters:
|
||||
a.params = parameters
|
||||
return a
|
||||
|
||||
|
||||
# More classes could be added programmatically
|
||||
|
||||
def _class_from_schema(name, schema=None, schema_file=None, base_classes=None):
|
||||
base_classes = base_classes or []
|
||||
base_classes.append(BaseModel)
|
||||
attrs = {}
|
||||
if schema:
|
||||
attrs['schema'] = schema
|
||||
elif schema_file:
|
||||
attrs['schema_file'] = schema_file
|
||||
else:
|
||||
attrs['schema'] = name
|
||||
name = "".join((name[0].upper(), name[1:]))
|
||||
return BaseMeta(name, base_classes, attrs)
|
||||
|
||||
|
||||
def _add_class_from_schema(*args, **kwargs):
|
||||
generatedClass = _class_from_schema(*args, **kwargs)
|
||||
globals()[generatedClass.__name__] = generatedClass
|
||||
del generatedClass
|
||||
|
@ -1,3 +1,5 @@
|
||||
#!/usr/local/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
from future import standard_library
|
||||
standard_library.install_aliases()
|
||||
|
||||
@ -17,7 +19,12 @@ import subprocess
|
||||
import importlib
|
||||
import yaml
|
||||
import threading
|
||||
import multiprocessing
|
||||
import pkg_resources
|
||||
from nltk import download
|
||||
from textwrap import dedent
|
||||
from sklearn.base import TransformerMixin, BaseEstimator
|
||||
from itertools import product
|
||||
|
||||
from .. import models, utils
|
||||
from .. import api
|
||||
@ -31,17 +38,19 @@ class PluginMeta(models.BaseMeta):
|
||||
_classes = {}
|
||||
|
||||
def __new__(mcs, name, bases, attrs, **kwargs):
|
||||
plugin_type = []
|
||||
if hasattr(bases[0], 'plugin_type'):
|
||||
plugin_type += bases[0].plugin_type
|
||||
plugin_type.append(name)
|
||||
alias = attrs.get('name', name)
|
||||
attrs['plugin_type'] = plugin_type
|
||||
plugin_type = set()
|
||||
for base in bases:
|
||||
if hasattr(base, '_plugin_type'):
|
||||
plugin_type |= base._plugin_type
|
||||
plugin_type.add(name)
|
||||
alias = attrs.get('name', name).lower()
|
||||
attrs['_plugin_type'] = plugin_type
|
||||
logger.debug('Adding new plugin class', name, bases, attrs, plugin_type)
|
||||
attrs['name'] = alias
|
||||
if 'description' not in attrs:
|
||||
doc = attrs.get('__doc__', None)
|
||||
if doc:
|
||||
attrs['description'] = doc
|
||||
attrs['description'] = dedent(doc)
|
||||
else:
|
||||
logger.warning(
|
||||
('Plugin {} does not have a description. '
|
||||
@ -77,6 +86,9 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
||||
|
||||
'''
|
||||
|
||||
_terse_keys = ['name', '@id', '@type', 'author', 'description',
|
||||
'extra_params', 'is_activated', 'url', 'version']
|
||||
|
||||
def __init__(self, info=None, data_folder=None, **kwargs):
|
||||
"""
|
||||
Provides a canonical name for plugins and serves as base for other
|
||||
@ -126,33 +138,42 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
||||
def get_folder(self):
|
||||
return os.path.dirname(inspect.getfile(self.__class__))
|
||||
|
||||
def _activate(self):
|
||||
self.activate()
|
||||
self.is_activated = True
|
||||
|
||||
def _deactivate(self):
|
||||
self.is_activated = False
|
||||
self.deactivate()
|
||||
|
||||
def activate(self):
|
||||
pass
|
||||
|
||||
def deactivate(self):
|
||||
pass
|
||||
|
||||
def process(self, request, parameters, **kwargs):
|
||||
def process(self, request, activity, **kwargs):
|
||||
"""
|
||||
An implemented plugin should override this method.
|
||||
Here, we assume that a process_entries method exists."""
|
||||
Here, we assume that a process_entries method exists.
|
||||
"""
|
||||
newentries = list(
|
||||
self.process_entries(request.entries, parameters))
|
||||
self.process_entries(request.entries, activity))
|
||||
request.entries = newentries
|
||||
return request
|
||||
|
||||
def process_entries(self, entries, parameters):
|
||||
def process_entries(self, entries, activity):
|
||||
for entry in entries:
|
||||
self.log.debug('Processing entry with plugin {}: {}'.format(
|
||||
self, entry))
|
||||
results = self.process_entry(entry, parameters)
|
||||
results = self.process_entry(entry, activity)
|
||||
if inspect.isgenerator(results):
|
||||
for result in results:
|
||||
yield result
|
||||
else:
|
||||
yield results
|
||||
|
||||
def process_entry(self, entry, parameters):
|
||||
def process_entry(self, entry, activity):
|
||||
"""
|
||||
This base method is here to adapt plugins which only
|
||||
implement the *process* function.
|
||||
@ -173,9 +194,12 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
||||
test_cases = self.test_cases
|
||||
for case in test_cases:
|
||||
try:
|
||||
fmt = 'case: {}'.format(case.get('name', case))
|
||||
if 'name' in case:
|
||||
self.log.info('Test case: {}'.format(case['name']))
|
||||
self.log.debug('Test case:\n\t{}'.format(
|
||||
pprint.pformat(fmt)))
|
||||
self.test_case(case)
|
||||
self.log.debug('Test case passed:\n{}'.format(
|
||||
pprint.pformat(case)))
|
||||
except Exception as ex:
|
||||
self.log.warning('Test case failed:\n{}'.format(
|
||||
pprint.pformat(case)))
|
||||
@ -200,7 +224,9 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
||||
entry,
|
||||
]
|
||||
|
||||
method = partial(self.process, request, parameters)
|
||||
activity = self.activity(parameters)
|
||||
|
||||
method = partial(self.process, request, activity)
|
||||
|
||||
if mock:
|
||||
res = method()
|
||||
@ -243,34 +269,41 @@ class Plugin(with_metaclass(PluginMeta, models.Plugin)):
|
||||
SenpyPlugin = Plugin
|
||||
|
||||
|
||||
class Analysis(Plugin):
|
||||
class Analyser(Plugin):
|
||||
'''
|
||||
A subclass of Plugin that analyses text and provides an annotation.
|
||||
'''
|
||||
|
||||
def analyse(self, request, parameters):
|
||||
return super(Analysis, self).process(request, parameters)
|
||||
# Deprecated
|
||||
def analyse(self, request, activity):
|
||||
return super(Analyser, self).process(request, activity)
|
||||
|
||||
def analyse_entries(self, entries, parameters):
|
||||
for i in super(Analysis, self).process_entries(entries, parameters):
|
||||
# Deprecated
|
||||
def analyse_entries(self, entries, activity):
|
||||
for i in super(Analyser, self).process_entries(entries, activity):
|
||||
yield i
|
||||
|
||||
def process(self, request, parameters, **kwargs):
|
||||
return self.analyse(request, parameters)
|
||||
def process(self, request, activity, **kwargs):
|
||||
return self.analyse(request, activity)
|
||||
|
||||
def process_entries(self, entries, parameters):
|
||||
for i in self.analyse_entries(entries, parameters):
|
||||
def process_entries(self, entries, activity):
|
||||
for i in self.analyse_entries(entries, activity):
|
||||
yield i
|
||||
|
||||
def process_entry(self, entry, parameters, **kwargs):
|
||||
def process_entry(self, entry, activity, **kwargs):
|
||||
if hasattr(self, 'analyse_entry'):
|
||||
for i in self.analyse_entry(entry, parameters):
|
||||
for i in self.analyse_entry(entry, activity):
|
||||
yield i
|
||||
else:
|
||||
super(Analysis, self).process_entry(entry, parameters, **kwargs)
|
||||
super(Analyser, self).process_entry(entry, activity, **kwargs)
|
||||
|
||||
|
||||
AnalysisPlugin = Analysis
|
||||
AnalysisPlugin = Analyser
|
||||
|
||||
|
||||
class Transformation(AnalysisPlugin):
|
||||
'''Empty'''
|
||||
pass
|
||||
|
||||
|
||||
class Conversion(Plugin):
|
||||
@ -297,32 +330,79 @@ class Conversion(Plugin):
|
||||
ConversionPlugin = Conversion
|
||||
|
||||
|
||||
class SentimentPlugin(Analysis, models.SentimentPlugin):
|
||||
class Evaluable(Plugin):
|
||||
'''
|
||||
Common class for plugins that can be evaluated with GSITK.
|
||||
|
||||
They should implement the methods below.
|
||||
'''
|
||||
|
||||
def as_pipe(self):
|
||||
raise Exception('Implement the as_pipe function')
|
||||
|
||||
def evaluate_func(self, X, activity=None):
|
||||
raise Exception('Implement the evaluate_func function')
|
||||
|
||||
|
||||
class SentimentPlugin(Analyser, Evaluable, models.SentimentPlugin):
|
||||
'''
|
||||
Sentiment plugins provide sentiment annotation (using Marl)
|
||||
'''
|
||||
minPolarityValue = 0
|
||||
maxPolarityValue = 1
|
||||
|
||||
_terse_keys = Analyser._terse_keys + ['minPolarityValue', 'maxPolarityValue']
|
||||
|
||||
def test_case(self, case):
|
||||
if 'polarity' in case:
|
||||
expected = case.get('expected', {})
|
||||
s = models.Sentiment(_auto_id=False)
|
||||
s.marl__hasPolarity = case['polarity']
|
||||
if 'sentiments' not in expected:
|
||||
expected['sentiments'] = []
|
||||
expected['sentiments'].append(s)
|
||||
if 'marl:hasOpinion' not in expected:
|
||||
expected['marl:hasOpinion'] = []
|
||||
expected['marl:hasOpinion'].append(s)
|
||||
case['expected'] = expected
|
||||
super(SentimentPlugin, self).test_case(case)
|
||||
|
||||
def normalize(self, value, minValue, maxValue):
|
||||
nv = minValue + (value - self.minPolarityValue) * (
|
||||
self.maxPolarityValue - self.minPolarityValue) / (maxValue - minValue)
|
||||
return nv
|
||||
|
||||
class EmotionPlugin(Analysis, models.EmotionPlugin):
|
||||
def as_pipe(self):
|
||||
pipe = gsitk_compat.Pipeline([('senpy-plugin', ScikitWrapper(self))])
|
||||
pipe.name = self.id
|
||||
return pipe
|
||||
|
||||
def evaluate_func(self, X, activity=None):
|
||||
if activity is None:
|
||||
parameters = api.parse_params({},
|
||||
self.extra_params)
|
||||
activity = self.activity(parameters)
|
||||
entries = []
|
||||
for feat in X:
|
||||
entries.append(models.Entry(nif__isString=feat[0]))
|
||||
labels = []
|
||||
for e in self.process_entries(entries, activity):
|
||||
sent = e.sentiments[0].polarity
|
||||
label = -1
|
||||
if sent == 'marl:Positive':
|
||||
label = 1
|
||||
elif sent == 'marl:Negative':
|
||||
label = -1
|
||||
labels.append(label)
|
||||
return labels
|
||||
|
||||
|
||||
class EmotionPlugin(Analyser, models.EmotionPlugin):
|
||||
'''
|
||||
Emotion plugins provide emotion annotation (using Onyx)
|
||||
'''
|
||||
minEmotionValue = 0
|
||||
maxEmotionValue = 1
|
||||
|
||||
_terse_keys = Analyser._terse_keys + ['minEmotionValue', 'maxEmotionValue']
|
||||
|
||||
|
||||
class EmotionConversion(Conversion):
|
||||
'''
|
||||
@ -345,69 +425,67 @@ EmotionConversionPlugin = EmotionConversion
|
||||
|
||||
|
||||
class PostProcessing(Plugin):
|
||||
'''
|
||||
A plugin that converts the output of other plugins (post-processing).
|
||||
'''
|
||||
def check(self, request, plugins):
|
||||
'''Should this plugin be run for this request?'''
|
||||
return False
|
||||
|
||||
|
||||
class Box(AnalysisPlugin):
|
||||
class Box(Analyser):
|
||||
'''
|
||||
Black box plugins delegate analysis to a function.
|
||||
The flow is like so:
|
||||
The flow is like this:
|
||||
|
||||
.. code-block::
|
||||
|
||||
entry --> input() --> predict_one() --> output() --> entry'
|
||||
entries --> to_features() --> predict_many() --> to_entry() --> entries'
|
||||
|
||||
|
||||
In other words: their ``input`` method convers a query (entry and a set of parameters) into
|
||||
the input to the box method. The ``output`` method convers the results given by the box into
|
||||
an entry that senpy can handle.
|
||||
In other words: their ``to_features`` method converts a query (entry and a set of parameters)
|
||||
into the input to the `predict_one` method, which only uses an array of features.
|
||||
The ``to_entry`` method converts the results given by the box into an entry that senpy can
|
||||
handle.
|
||||
'''
|
||||
|
||||
def input(self, entry, params=None):
|
||||
def to_features(self, entry, activity=None):
|
||||
'''Transforms a query (entry+param) into an input for the black box'''
|
||||
return entry
|
||||
|
||||
def output(self, output, entry=None, params=None):
|
||||
def to_entry(self, features, entry=None, activity=None):
|
||||
'''Transforms the results of the black box into an entry'''
|
||||
return output
|
||||
return entry
|
||||
|
||||
def predict_one(self, input):
|
||||
def predict_one(self, features, activity=None):
|
||||
raise NotImplementedError(
|
||||
'You should define the behavior of this plugin')
|
||||
|
||||
def process_entries(self, entries, params):
|
||||
def predict_many(self, features, activity=None):
|
||||
results = []
|
||||
for feat in features:
|
||||
results.append(self.predict_one(features=feat, activity=activity))
|
||||
return results
|
||||
|
||||
def process_entry(self, entry, activity):
|
||||
for i in self.process_entries([entry], activity):
|
||||
yield i
|
||||
|
||||
def process_entries(self, entries, activity):
|
||||
features = []
|
||||
for entry in entries:
|
||||
input = self.input(entry=entry, params=params)
|
||||
results = self.predict_one(input=input)
|
||||
yield self.output(output=results, entry=entry, params=params)
|
||||
features.append(self.to_features(entry=entry, activity=activity))
|
||||
results = self.predict_many(features=features, activity=activity)
|
||||
|
||||
def fit(self, X=None, y=None):
|
||||
return self
|
||||
|
||||
def transform(self, X):
|
||||
return [self.predict_one(x) for x in X]
|
||||
|
||||
def predict(self, X):
|
||||
return self.transform(X)
|
||||
|
||||
def fit_transform(self, X, y):
|
||||
self.fit(X, y)
|
||||
return self.transform(X)
|
||||
|
||||
def as_pipe(self):
|
||||
pipe = gsitk_compat.Pipeline([('plugin', self)])
|
||||
pipe.name = self.name
|
||||
return pipe
|
||||
for (result, entry) in zip(results, entries):
|
||||
yield self.to_entry(features=result, entry=entry, activity=activity)
|
||||
|
||||
|
||||
class TextBox(Box):
|
||||
'''A black box plugin that takes only text as input'''
|
||||
|
||||
def input(self, entry, params):
|
||||
entry = super(TextBox, self).input(entry, params)
|
||||
return entry['nif:isString']
|
||||
def to_features(self, entry, activity):
|
||||
return [entry['nif:isString']]
|
||||
|
||||
|
||||
class SentimentBox(TextBox, SentimentPlugin):
|
||||
@ -415,17 +493,35 @@ class SentimentBox(TextBox, SentimentPlugin):
|
||||
A box plugin where the output is only a polarity label or a tuple (polarity, polarityValue)
|
||||
'''
|
||||
|
||||
def output(self, output, entry, **kwargs):
|
||||
s = models.Sentiment()
|
||||
try:
|
||||
label, value = output
|
||||
except ValueError:
|
||||
label, value = output, None
|
||||
s.prov(self)
|
||||
s.polarity = label
|
||||
if value is not None:
|
||||
s.polarityValue = value
|
||||
entry.sentiments.append(s)
|
||||
classes = ['marl:Positive', 'marl:Neutral', 'marl:Negative']
|
||||
binary = True
|
||||
|
||||
def to_entry(self, features, entry, activity, **kwargs):
|
||||
|
||||
if len(features) != len(self.classes):
|
||||
raise models.Error('The number of features ({}) does not match the classes '
|
||||
'(plugin.classes ({})'.format(len(features), len(self.classes)))
|
||||
|
||||
minValue = activity.param('marl:minPolarityValue', 0)
|
||||
maxValue = activity.param('marl:minPolarityValue', 1)
|
||||
activity['marl:minPolarityValue'] = minValue
|
||||
activity['marl:maxPolarityValue'] = maxValue
|
||||
|
||||
for k, v in zip(self.classes, features):
|
||||
s = models.Sentiment()
|
||||
if self.binary:
|
||||
if not v: # Carry on if the value is 0
|
||||
continue
|
||||
s['marl:hasPolarity'] = k
|
||||
else:
|
||||
if v is not None:
|
||||
s['marl:hasPolarity'] = k
|
||||
nv = self.normalize(v, minValue, maxValue)
|
||||
s['marl:polarityValue'] = nv
|
||||
s.prov(activity)
|
||||
|
||||
entry.sentiments.append(s)
|
||||
|
||||
return entry
|
||||
|
||||
|
||||
@ -434,14 +530,23 @@ class EmotionBox(TextBox, EmotionPlugin):
|
||||
A box plugin where the output is only an a tuple of emotion labels
|
||||
'''
|
||||
|
||||
def output(self, output, entry, **kwargs):
|
||||
if not isinstance(output, list):
|
||||
output = [output]
|
||||
EMOTIONS = []
|
||||
with_intensity = True
|
||||
|
||||
def to_entry(self, features, entry, activity, **kwargs):
|
||||
s = models.EmotionSet()
|
||||
entry.emotions.append(s)
|
||||
for label in output:
|
||||
|
||||
if len(features) != len(self.EMOTIONS):
|
||||
raise Exception(('The number of classes in the plugin and the number of features '
|
||||
'do not match'))
|
||||
|
||||
for label, intensity in zip(self.EMOTIONS, features):
|
||||
e = models.Emotion(onyx__hasEmotionCategory=label)
|
||||
s.append(e)
|
||||
if self.with_intensity:
|
||||
e.onyx__hasEmotionIntensity = intensity
|
||||
s.onyx__hasEmotion.append(e)
|
||||
s.prov(activity)
|
||||
entry.emotions.append(s)
|
||||
return entry
|
||||
|
||||
|
||||
@ -454,11 +559,15 @@ class MappingMixin(object):
|
||||
def mappings(self, value):
|
||||
self._mappings = value
|
||||
|
||||
def output(self, output, entry, params):
|
||||
output = self.mappings.get(output, self.mappings.get(
|
||||
'default', output))
|
||||
return super(MappingMixin, self).output(
|
||||
output=output, entry=entry, params=params)
|
||||
def to_entry(self, features, entry, activity):
|
||||
features = list(features)
|
||||
for i, feat in enumerate(features):
|
||||
features[i] = self.mappings.get(feat,
|
||||
self.mappings.get('default',
|
||||
feat))
|
||||
return super(MappingMixin, self).to_entry(features=features,
|
||||
entry=entry,
|
||||
activity=activity)
|
||||
|
||||
|
||||
class ShelfMixin(object):
|
||||
@ -505,7 +614,7 @@ class ShelfMixin(object):
|
||||
pickle.dump(self._sh, f)
|
||||
|
||||
|
||||
def pfilter(plugins, plugin_type=Analysis, **kwargs):
|
||||
def pfilter(plugins, plugin_type=Analyser, **kwargs):
|
||||
""" Filter plugins by different criteria """
|
||||
if isinstance(plugins, models.Plugins):
|
||||
plugins = plugins.plugins
|
||||
@ -526,6 +635,9 @@ def pfilter(plugins, plugin_type=Analysis, **kwargs):
|
||||
else:
|
||||
candidates = plugins
|
||||
|
||||
if 'name' in kwargs:
|
||||
kwargs['name'] = kwargs['name'].lower()
|
||||
|
||||
logger.debug(candidates)
|
||||
|
||||
def matches(plug):
|
||||
@ -549,31 +661,48 @@ def load_module(name, root=None):
|
||||
|
||||
def _log_subprocess_output(process):
|
||||
for line in iter(process.stdout.readline, b''):
|
||||
logger.info('%r', line)
|
||||
logger.info('%s', line.decode())
|
||||
for line in iter(process.stderr.readline, b''):
|
||||
logger.error('%r', line)
|
||||
logger.error('%s', line.decode())
|
||||
|
||||
|
||||
def missing_requirements(reqs):
|
||||
queue = []
|
||||
pool = multiprocessing.Pool(4)
|
||||
for req in reqs:
|
||||
res = pool.apply_async(pkg_resources.get_distribution, (req,))
|
||||
queue.append((req, res))
|
||||
missing = []
|
||||
for req, job in queue:
|
||||
try:
|
||||
job.get(1)
|
||||
except Exception:
|
||||
missing.append(req)
|
||||
return missing
|
||||
|
||||
|
||||
def install_deps(*plugins):
|
||||
installed = False
|
||||
nltk_resources = set()
|
||||
requirements = []
|
||||
for info in plugins:
|
||||
requirements = info.get('requirements', [])
|
||||
if requirements:
|
||||
pip_args = [sys.executable, '-m', 'pip', 'install']
|
||||
for req in requirements:
|
||||
pip_args.append(req)
|
||||
logger.info('Installing requirements: ' + str(requirements))
|
||||
process = subprocess.Popen(
|
||||
pip_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
_log_subprocess_output(process)
|
||||
exitcode = process.wait()
|
||||
installed = True
|
||||
if exitcode != 0:
|
||||
raise models.Error(
|
||||
"Dependencies not properly installed: {}".format(pip_args))
|
||||
requirements += missing_requirements(requirements)
|
||||
nltk_resources |= set(info.get('nltk_resources', []))
|
||||
|
||||
if requirements:
|
||||
logger.info('Installing requirements: ' + str(requirements))
|
||||
pip_args = [sys.executable, '-m', 'pip', 'install']
|
||||
for req in requirements:
|
||||
pip_args.append(req)
|
||||
process = subprocess.Popen(
|
||||
pip_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
_log_subprocess_output(process)
|
||||
exitcode = process.wait()
|
||||
installed = True
|
||||
if exitcode != 0:
|
||||
raise models.Error(
|
||||
"Dependencies not properly installed: {}".format(pip_args))
|
||||
installed |= download(list(nltk_resources))
|
||||
return installed
|
||||
|
||||
@ -632,7 +761,7 @@ def from_info(info, root=None, install_on_fail=True, **kwargs):
|
||||
def parse_plugin_info(fpath):
|
||||
logger.debug("Parsing plugin info: {}".format(fpath))
|
||||
with open(fpath, 'r') as f:
|
||||
info = yaml.load(f)
|
||||
info = yaml.load(f, Loader=yaml.FullLoader)
|
||||
info['_path'] = fpath
|
||||
return info
|
||||
|
||||
@ -688,14 +817,34 @@ def _from_loaded_module(module, info=None, **kwargs):
|
||||
yield instance
|
||||
|
||||
|
||||
cached_evs = {}
|
||||
|
||||
|
||||
def evaluate(plugins, datasets, **kwargs):
|
||||
ev = gsitk_compat.Eval(
|
||||
tuples=None,
|
||||
datasets=datasets,
|
||||
pipelines=[plugin.as_pipe() for plugin in plugins])
|
||||
ev.evaluate()
|
||||
results = ev.results
|
||||
evaluations = evaluations_to_JSONLD(results, **kwargs)
|
||||
for plug in plugins:
|
||||
if not hasattr(plug, 'as_pipe'):
|
||||
raise models.Error('Plugin {} cannot be evaluated'.format(plug.name))
|
||||
|
||||
tuples = list(product(plugins, datasets))
|
||||
missing = []
|
||||
for (p, d) in tuples:
|
||||
if (p.id, d) not in cached_evs:
|
||||
pipe = p.as_pipe()
|
||||
missing.append(gsitk_compat.EvalPipeline(pipe, d))
|
||||
if missing:
|
||||
ev = gsitk_compat.Eval(tuples=missing, datasets=datasets)
|
||||
ev.evaluate()
|
||||
results = ev.results
|
||||
new_ev = evaluations_to_JSONLD(results, **kwargs)
|
||||
for ev in new_ev:
|
||||
dataset = ev.evaluatesOn
|
||||
model = ev.evaluates.rstrip('__' + dataset)
|
||||
cached_evs[(model, dataset)] = ev
|
||||
evaluations = []
|
||||
print(tuples, 'Cached evs', cached_evs)
|
||||
for (p, d) in tuples:
|
||||
print('Adding', d, p)
|
||||
evaluations.append(cached_evs[(p.id, d)])
|
||||
return evaluations
|
||||
|
||||
|
||||
@ -708,7 +857,7 @@ def evaluations_to_JSONLD(results, flatten=False):
|
||||
metric_names = ['accuracy', 'precision_macro', 'recall_macro',
|
||||
'f1_macro', 'f1_weighted', 'f1_micro', 'f1_macro']
|
||||
|
||||
for index, row in results.iterrows():
|
||||
for index, row in results.fillna('Not Available').iterrows():
|
||||
evaluation = models.Evaluation()
|
||||
if row.get('CV', True):
|
||||
evaluation['@type'] = ['StaticCV', 'Evaluation']
|
||||
@ -724,10 +873,29 @@ def evaluations_to_JSONLD(results, flatten=False):
|
||||
# We should probably discontinue this representation
|
||||
for name in metric_names:
|
||||
metric = models.Metric()
|
||||
metric['@id'] = 'Metric' + str(i)
|
||||
metric['@type'] = name.capitalize()
|
||||
metric.value = row[name]
|
||||
evaluation.metrics.append(metric)
|
||||
i += 1
|
||||
evaluations.append(evaluation)
|
||||
return evaluations
|
||||
|
||||
|
||||
class ScikitWrapper(BaseEstimator, TransformerMixin):
|
||||
def __init__(self, plugin=None):
|
||||
self.plugin = plugin
|
||||
|
||||
def fit(self, X=None, y=None):
|
||||
if self.plugin is not None and not self.plugin.is_activated:
|
||||
self.plugin.activate()
|
||||
return self
|
||||
|
||||
def transform(self, X):
|
||||
return self.plugin.evaluate_func(X, None)
|
||||
|
||||
def predict(self, X):
|
||||
return self.transform(X)
|
||||
|
||||
def fit_transform(self, X, y):
|
||||
self.fit(X, y)
|
||||
return self.transform(X)
|
||||
|
@ -1,34 +0,0 @@
|
||||
import random
|
||||
|
||||
from senpy.plugins import EmotionPlugin
|
||||
from senpy.models import EmotionSet, Emotion, Entry
|
||||
|
||||
|
||||
class EmoRand(EmotionPlugin):
|
||||
name = "emoRand"
|
||||
description = 'A sample plugin that returns a random emotion annotation'
|
||||
author = '@balkian'
|
||||
version = '0.1'
|
||||
url = "https://github.com/gsi-upm/senpy-plugins-community"
|
||||
requirements = {}
|
||||
onyx__usesEmotionModel = "emoml:big6"
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
category = "emoml:big6happiness"
|
||||
number = max(-1, min(1, random.gauss(0, 0.5)))
|
||||
if number > 0:
|
||||
category = "emoml:big6anger"
|
||||
emotionSet = EmotionSet()
|
||||
emotion = Emotion({"onyx:hasEmotionCategory": category})
|
||||
emotionSet.onyx__hasEmotion.append(emotion)
|
||||
emotionSet.prov__wasGeneratedBy = self.id
|
||||
entry.emotions.append(emotionSet)
|
||||
yield entry
|
||||
|
||||
def test(self):
|
||||
params = dict()
|
||||
results = list()
|
||||
for i in range(100):
|
||||
res = next(self.analyse_entry(Entry(nif__isString="Hello"), params))
|
||||
res.validate()
|
||||
results.append(res.emotions[0]['onyx:hasEmotion'][0]['onyx:hasEmotionCategory'])
|
@ -1,19 +1,26 @@
|
||||
from senpy.plugins import AnalysisPlugin
|
||||
from senpy.plugins import Transformation
|
||||
from senpy.models import Entry
|
||||
from nltk.tokenize.punkt import PunktSentenceTokenizer
|
||||
from nltk.tokenize.simple import LineTokenizer
|
||||
import nltk
|
||||
|
||||
|
||||
class Split(AnalysisPlugin):
|
||||
'''description: A sample plugin that chunks input text'''
|
||||
class Split(Transformation):
|
||||
'''
|
||||
A plugin that chunks input text, into paragraphs or sentences.
|
||||
|
||||
It does not provide any sort of annotation, and it is meant to precede
|
||||
other annotation plugins, when the annotation of individual sentences
|
||||
(or paragraphs) is required.
|
||||
'''
|
||||
|
||||
author = ["@militarpancho", '@balkian']
|
||||
version = '0.3'
|
||||
url = "https://github.com/gsi-upm/senpy"
|
||||
nltk_resources = ['punkt']
|
||||
|
||||
extra_params = {
|
||||
'delimiter': {
|
||||
'description': 'Split text into paragraphs or sentences.',
|
||||
'aliases': ['type', 't'],
|
||||
'required': False,
|
||||
'default': 'sentence',
|
||||
@ -21,12 +28,9 @@ class Split(AnalysisPlugin):
|
||||
},
|
||||
}
|
||||
|
||||
def activate(self):
|
||||
nltk.download('punkt')
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
def analyse_entry(self, entry, activity):
|
||||
yield entry
|
||||
chunker_type = params["delimiter"]
|
||||
chunker_type = activity.params["delimiter"]
|
||||
original_text = entry['nif:isString']
|
||||
if chunker_type == "sentence":
|
||||
tokenizer = PunktSentenceTokenizer()
|
||||
|
@ -103,7 +103,9 @@ class CentroidConversion(EmotionConversionPlugin):
|
||||
for i in emotionSet.onyx__hasEmotion:
|
||||
e.onyx__hasEmotion.append(self._backwards_conversion(i))
|
||||
else:
|
||||
raise Error('EMOTION MODEL NOT KNOWN')
|
||||
raise Error('EMOTION MODEL NOT KNOWN. '
|
||||
'Cannot convert from {} to {}'.format(fromModel,
|
||||
toModel))
|
||||
yield e
|
||||
|
||||
def test(self, info=None):
|
||||
|
@ -31,7 +31,7 @@ centroids_direction:
|
||||
- emoml:pad
|
||||
aliases: # These are aliases for any key in the centroid, to avoid repeating a long name several times
|
||||
A: emoml:pad-dimensions:arousal
|
||||
V: emoml:pad-dimensions:pleasure
|
||||
V: emoml:pad-dimensions:valence
|
||||
D: emoml:pad-dimensions:dominance
|
||||
anger: emoml:big6anger
|
||||
disgust: emoml:big6disgust
|
||||
|
@ -6,7 +6,7 @@ class MaxEmotion(PostProcessing):
|
||||
author = '@dsuarezsouto'
|
||||
version = '0.1'
|
||||
|
||||
def process_entry(self, entry, params):
|
||||
def process_entry(self, entry, activity):
|
||||
if len(entry.emotions) < 1:
|
||||
yield entry
|
||||
return
|
||||
@ -32,7 +32,7 @@ class MaxEmotion(PostProcessing):
|
||||
|
||||
entry.emotions[0]['onyx:hasEmotion'] = [max_emotion]
|
||||
|
||||
entry.emotions[0]['prov:wasGeneratedBy'] = "maxSentiment"
|
||||
entry.emotions[0]['prov:wasGeneratedBy'] = activity.id
|
||||
yield entry
|
||||
|
||||
def check(self, request, plugins):
|
||||
@ -43,12 +43,11 @@ class MaxEmotion(PostProcessing):
|
||||
# 2 Case to return a Neutral Emotion.
|
||||
test_cases = [
|
||||
{
|
||||
"name":
|
||||
"If there are several emotions within an emotion set, reduce it to one.",
|
||||
"name": "If there are several emotions within an emotion set, reduce it to one.",
|
||||
"entry": {
|
||||
"@type":
|
||||
"entry",
|
||||
"emotions": [
|
||||
"onyx:hasEmotionSet": [
|
||||
{
|
||||
"@id":
|
||||
"Emotions0",
|
||||
@ -94,7 +93,7 @@ class MaxEmotion(PostProcessing):
|
||||
'expected': {
|
||||
"@type":
|
||||
"entry",
|
||||
"emotions": [
|
||||
"onyx:hasEmotionSet": [
|
||||
{
|
||||
"@id":
|
||||
"Emotions0",
|
||||
@ -107,9 +106,7 @@ class MaxEmotion(PostProcessing):
|
||||
"onyx:hasEmotionCategory": "joy",
|
||||
"onyx:hasEmotionIntensity": 0.3333333333333333
|
||||
}
|
||||
],
|
||||
"prov:wasGeneratedBy":
|
||||
'maxSentiment'
|
||||
]
|
||||
}
|
||||
],
|
||||
"nif:isString":
|
||||
@ -122,7 +119,7 @@ class MaxEmotion(PostProcessing):
|
||||
"entry": {
|
||||
"@type":
|
||||
"entry",
|
||||
"emotions": [{
|
||||
"onyx:hasEmotionSet": [{
|
||||
"@id":
|
||||
"Emotions0",
|
||||
"@type":
|
||||
@ -171,7 +168,7 @@ class MaxEmotion(PostProcessing):
|
||||
'expected': {
|
||||
"@type":
|
||||
"entry",
|
||||
"emotions": [{
|
||||
"onyx:hasEmotionSet": [{
|
||||
"@id":
|
||||
"Emotions0",
|
||||
"@type":
|
||||
@ -181,9 +178,7 @@ class MaxEmotion(PostProcessing):
|
||||
"@type": "emotion",
|
||||
"onyx:hasEmotionCategory": "neutral",
|
||||
"onyx:hasEmotionIntensity": 1
|
||||
}],
|
||||
"prov:wasGeneratedBy":
|
||||
'maxSentiment'
|
||||
}]
|
||||
}],
|
||||
"nif:isString":
|
||||
"Test"
|
||||
|
@ -1,13 +1,12 @@
|
||||
import requests
|
||||
import json
|
||||
|
||||
from senpy.plugins import SentimentPlugin
|
||||
from senpy.models import Sentiment
|
||||
from senpy.plugins import SentimentBox
|
||||
|
||||
ENDPOINT = 'http://www.sentiment140.com/api/bulkClassifyJson'
|
||||
|
||||
|
||||
class Sentiment140(SentimentPlugin):
|
||||
class Sentiment140(SentimentBox):
|
||||
'''Connects to the sentiment140 free API: http://sentiment140.com'''
|
||||
|
||||
author = "@balkian"
|
||||
@ -16,43 +15,40 @@ class Sentiment140(SentimentPlugin):
|
||||
extra_params = {
|
||||
'language': {
|
||||
"@id": 'lang_sentiment140',
|
||||
'description': 'language of the text',
|
||||
'aliases': ['language', 'l'],
|
||||
'required': False,
|
||||
'required': True,
|
||||
'default': 'auto',
|
||||
'options': ['es', 'en', 'auto']
|
||||
}
|
||||
}
|
||||
|
||||
maxPolarityValue = 1
|
||||
minPolarityValue = 0
|
||||
classes = ['marl:Positive', 'marl:Neutral', 'marl:Negative']
|
||||
binary = True
|
||||
|
||||
def predict_many(self, features, activity):
|
||||
lang = activity.params["language"]
|
||||
data = []
|
||||
|
||||
for feature in features:
|
||||
data.append({'text': feature[0]})
|
||||
|
||||
def analyse_entry(self, entry, params):
|
||||
lang = params["language"]
|
||||
res = requests.post(ENDPOINT,
|
||||
json.dumps({
|
||||
"language": lang,
|
||||
"data": [{
|
||||
"text": entry['nif:isString']
|
||||
}]
|
||||
"data": data
|
||||
}))
|
||||
p = params.get("prefix", None)
|
||||
polarity_value = self.maxPolarityValue * int(
|
||||
res.json()["data"][0]["polarity"]) * 0.25
|
||||
polarity = "marl:Neutral"
|
||||
neutral_value = self.maxPolarityValue / 2.0
|
||||
if polarity_value > neutral_value:
|
||||
polarity = "marl:Positive"
|
||||
elif polarity_value < neutral_value:
|
||||
polarity = "marl:Negative"
|
||||
|
||||
sentiment = Sentiment(
|
||||
prefix=p,
|
||||
marl__hasPolarity=polarity,
|
||||
marl__polarityValue=polarity_value)
|
||||
sentiment.prov__wasGeneratedBy = self.id
|
||||
entry.sentiments.append(sentiment)
|
||||
entry.language = lang
|
||||
yield entry
|
||||
for res in res.json()["data"]:
|
||||
polarity = int(res['polarity'])
|
||||
neutral_value = 2
|
||||
if polarity > neutral_value:
|
||||
yield [1, 0, 0]
|
||||
continue
|
||||
elif polarity < neutral_value:
|
||||
yield [0, 0, 1]
|
||||
continue
|
||||
yield [0, 1, 0]
|
||||
|
||||
test_cases = [
|
||||
{
|
||||
@ -62,7 +58,7 @@ class Sentiment140(SentimentPlugin):
|
||||
'params': {},
|
||||
'expected': {
|
||||
"nif:isString": "I love Titanic",
|
||||
'sentiments': [
|
||||
'marl:hasOpinion': [
|
||||
{
|
||||
'marl:hasPolarity': 'marl:Positive',
|
||||
}
|
||||
|
@ -11,14 +11,12 @@
|
||||
"$ref": "context.json"
|
||||
},
|
||||
"@type": {
|
||||
"default": "AggregatedEvaluation"
|
||||
},
|
||||
"@id": {
|
||||
"description": "ID of the aggregated evaluation",
|
||||
"type": "string"
|
||||
},
|
||||
"evaluations": {
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
|
@ -17,7 +17,6 @@
|
||||
"prov:used": {
|
||||
"description": "Parameters of the algorithm",
|
||||
"@type": "array",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "parameter.json"
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"@context": {
|
||||
"@vocab": "http://www.gsi.dit.upm.es/ontologies/senpy#",
|
||||
"@vocab": "http://www.gsi.upm.es/onto/senpy/ns#",
|
||||
"dc": "http://dublincore.org/2012/06/14/dcelements#",
|
||||
"me": "http://www.mixedemotions-project.eu/ns/model#",
|
||||
"senpy": "http://www.gsi.upm.es/onto/senpy/ns#",
|
||||
"prov": "http://www.w3.org/ns/prov#",
|
||||
"nif": "http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#",
|
||||
"marl": "http://www.gsi.dit.upm.es/ontologies/marl/ns#",
|
||||
@ -16,10 +16,10 @@
|
||||
"@container": "@set"
|
||||
},
|
||||
"entities": {
|
||||
"@id": "me:hasEntities"
|
||||
"@id": "senpy:hasEntities"
|
||||
},
|
||||
"suggestions": {
|
||||
"@id": "me:hasSuggestions",
|
||||
"@id": "senpy:hasSuggestions",
|
||||
"@container": "@set"
|
||||
},
|
||||
"onyx:hasEmotion": {
|
||||
@ -40,7 +40,7 @@
|
||||
"@id": "prov:used",
|
||||
"@container": "@set"
|
||||
},
|
||||
"analysis": {
|
||||
"activities": {
|
||||
"@id": "prov:wasInformedBy",
|
||||
"@type": "@id",
|
||||
"@container": "@set"
|
||||
@ -65,6 +65,13 @@
|
||||
},
|
||||
"onyx:conversionTo": {
|
||||
"@type": "@id"
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"@type": "Parameter"
|
||||
},
|
||||
"errors": {
|
||||
"@type": "ParameterError"
|
||||
},
|
||||
"prefix": "http://senpy.invalid/"
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@
|
||||
"properties": {
|
||||
"datasets": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "dataset.json"
|
||||
}
|
||||
|
@ -19,8 +19,7 @@
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "emotion.json"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["@id", "onyx:hasEmotion"]
|
||||
|
@ -12,8 +12,7 @@
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "emotion.json"
|
||||
},
|
||||
"default": []
|
||||
}
|
||||
},
|
||||
"prov:wasGeneratedBy": {
|
||||
"type": "string",
|
||||
|
@ -9,30 +9,13 @@
|
||||
"description": "String contained in this Context. Alternative: nif:isString",
|
||||
"type": "string"
|
||||
},
|
||||
"sentiments": {
|
||||
"marl:hasOpinion": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "sentiment.json" },
|
||||
"default": []
|
||||
"items": {"$ref": "sentiment.json" }
|
||||
},
|
||||
"emotions": {
|
||||
"onyx:hasEmotionSet": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "emotionSet.json" },
|
||||
"default": []
|
||||
},
|
||||
"entities": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "entity.json" },
|
||||
"default": []
|
||||
},
|
||||
"topics": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "topic.json" },
|
||||
"default": []
|
||||
},
|
||||
"suggestions": {
|
||||
"type": "array",
|
||||
"items": {"$ref": "suggestion.json" },
|
||||
"default": []
|
||||
"items": {"$ref": "emotionSet.json" }
|
||||
}
|
||||
},
|
||||
"required": ["nif:isString"]
|
||||
|
@ -6,8 +6,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"@type": {
|
||||
"type": "array",
|
||||
"default": "Evaluation"
|
||||
"type": "array"
|
||||
|
||||
},
|
||||
"metrics": {
|
||||
|
@ -24,8 +24,7 @@
|
||||
"description": "Sub-type of plugin. e.g. sentimentPlugin"
|
||||
},
|
||||
"extra_params": {
|
||||
"type": "object",
|
||||
"default": {}
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,15 @@
|
||||
"properties": {
|
||||
"plugins": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "plugin.json"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"$ref": "plugin.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"name": "Entry",
|
||||
"allOf": [
|
||||
{"$ref": "response.json"},
|
||||
{
|
||||
@ -10,15 +11,11 @@
|
||||
"@context": {
|
||||
"$ref": "context.json"
|
||||
},
|
||||
"@type": {
|
||||
"default": "results"
|
||||
},
|
||||
"@id": {
|
||||
"description": "ID of the analysis",
|
||||
"type": "string"
|
||||
},
|
||||
"analysis": {
|
||||
"default": [],
|
||||
"activities": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "analysis.json"
|
||||
@ -26,14 +23,13 @@
|
||||
},
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "entry.json"
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
"required": ["@id", "analysis", "entries"]
|
||||
"required": ["@id", "activities", "entries"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
347
senpy/static/css/bootstrap-theme.css
vendored
347
senpy/static/css/bootstrap-theme.css
vendored
@ -1,347 +0,0 @@
|
||||
/*!
|
||||
* Bootstrap v3.1.1 (http://getbootstrap.com)
|
||||
* Copyright 2011-2014 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
*/
|
||||
|
||||
.btn-default,
|
||||
.btn-primary,
|
||||
.btn-success,
|
||||
.btn-info,
|
||||
.btn-warning,
|
||||
.btn-danger {
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, .2);
|
||||
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075);
|
||||
}
|
||||
.btn-default:active,
|
||||
.btn-primary:active,
|
||||
.btn-success:active,
|
||||
.btn-info:active,
|
||||
.btn-warning:active,
|
||||
.btn-danger:active,
|
||||
.btn-default.active,
|
||||
.btn-primary.active,
|
||||
.btn-success.active,
|
||||
.btn-info.active,
|
||||
.btn-warning.active,
|
||||
.btn-danger.active {
|
||||
-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
|
||||
box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125);
|
||||
}
|
||||
.btn:active,
|
||||
.btn.active {
|
||||
background-image: none;
|
||||
}
|
||||
.btn-default {
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%);
|
||||
background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #dbdbdb;
|
||||
border-color: #ccc;
|
||||
}
|
||||
.btn-default:hover,
|
||||
.btn-default:focus {
|
||||
background-color: #e0e0e0;
|
||||
background-position: 0 -15px;
|
||||
}
|
||||
.btn-default:active,
|
||||
.btn-default.active {
|
||||
background-color: #e0e0e0;
|
||||
border-color: #dbdbdb;
|
||||
}
|
||||
.btn-primary {
|
||||
background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%);
|
||||
background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #2b669a;
|
||||
}
|
||||
.btn-primary:hover,
|
||||
.btn-primary:focus {
|
||||
background-color: #2d6ca2;
|
||||
background-position: 0 -15px;
|
||||
}
|
||||
.btn-primary:active,
|
||||
.btn-primary.active {
|
||||
background-color: #2d6ca2;
|
||||
border-color: #2b669a;
|
||||
}
|
||||
.btn-success {
|
||||
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);
|
||||
background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #3e8f3e;
|
||||
}
|
||||
.btn-success:hover,
|
||||
.btn-success:focus {
|
||||
background-color: #419641;
|
||||
background-position: 0 -15px;
|
||||
}
|
||||
.btn-success:active,
|
||||
.btn-success.active {
|
||||
background-color: #419641;
|
||||
border-color: #3e8f3e;
|
||||
}
|
||||
.btn-info {
|
||||
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);
|
||||
background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #28a4c9;
|
||||
}
|
||||
.btn-info:hover,
|
||||
.btn-info:focus {
|
||||
background-color: #2aabd2;
|
||||
background-position: 0 -15px;
|
||||
}
|
||||
.btn-info:active,
|
||||
.btn-info.active {
|
||||
background-color: #2aabd2;
|
||||
border-color: #28a4c9;
|
||||
}
|
||||
.btn-warning {
|
||||
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);
|
||||
background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #e38d13;
|
||||
}
|
||||
.btn-warning:hover,
|
||||
.btn-warning:focus {
|
||||
background-color: #eb9316;
|
||||
background-position: 0 -15px;
|
||||
}
|
||||
.btn-warning:active,
|
||||
.btn-warning.active {
|
||||
background-color: #eb9316;
|
||||
border-color: #e38d13;
|
||||
}
|
||||
.btn-danger {
|
||||
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);
|
||||
background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #b92c28;
|
||||
}
|
||||
.btn-danger:hover,
|
||||
.btn-danger:focus {
|
||||
background-color: #c12e2a;
|
||||
background-position: 0 -15px;
|
||||
}
|
||||
.btn-danger:active,
|
||||
.btn-danger.active {
|
||||
background-color: #c12e2a;
|
||||
border-color: #b92c28;
|
||||
}
|
||||
.thumbnail,
|
||||
.img-thumbnail {
|
||||
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
|
||||
}
|
||||
.dropdown-menu > li > a:hover,
|
||||
.dropdown-menu > li > a:focus {
|
||||
background-color: #e8e8e8;
|
||||
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
|
||||
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.dropdown-menu > .active > a,
|
||||
.dropdown-menu > .active > a:hover,
|
||||
.dropdown-menu > .active > a:focus {
|
||||
background-color: #357ebd;
|
||||
background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
|
||||
background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.navbar-default {
|
||||
background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%);
|
||||
background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
background-repeat: repeat-x;
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075);
|
||||
}
|
||||
.navbar-default .navbar-nav > .active > a {
|
||||
background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f3f3f3 100%);
|
||||
background-image: linear-gradient(to bottom, #ebebeb 0%, #f3f3f3 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff3f3f3', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
|
||||
box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075);
|
||||
}
|
||||
.navbar-brand,
|
||||
.navbar-nav > li > a {
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, .25);
|
||||
}
|
||||
.navbar-inverse {
|
||||
background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%);
|
||||
background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.navbar-inverse .navbar-nav > .active > a {
|
||||
background-image: -webkit-linear-gradient(top, #222 0%, #282828 100%);
|
||||
background-image: linear-gradient(to bottom, #222 0%, #282828 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff282828', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
|
||||
box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25);
|
||||
}
|
||||
.navbar-inverse .navbar-brand,
|
||||
.navbar-inverse .navbar-nav > li > a {
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, .25);
|
||||
}
|
||||
.navbar-static-top,
|
||||
.navbar-fixed-top,
|
||||
.navbar-fixed-bottom {
|
||||
border-radius: 0;
|
||||
}
|
||||
.alert {
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, .2);
|
||||
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05);
|
||||
}
|
||||
.alert-success {
|
||||
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);
|
||||
background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #b2dba1;
|
||||
}
|
||||
.alert-info {
|
||||
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);
|
||||
background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #9acfea;
|
||||
}
|
||||
.alert-warning {
|
||||
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);
|
||||
background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #f5e79e;
|
||||
}
|
||||
.alert-danger {
|
||||
background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);
|
||||
background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #dca7a7;
|
||||
}
|
||||
.progress {
|
||||
background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
|
||||
background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.progress-bar {
|
||||
background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%);
|
||||
background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.progress-bar-success {
|
||||
background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);
|
||||
background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.progress-bar-info {
|
||||
background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);
|
||||
background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.progress-bar-warning {
|
||||
background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);
|
||||
background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.progress-bar-danger {
|
||||
background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);
|
||||
background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.list-group {
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .075);
|
||||
}
|
||||
.list-group-item.active,
|
||||
.list-group-item.active:hover,
|
||||
.list-group-item.active:focus {
|
||||
text-shadow: 0 -1px 0 #3071a9;
|
||||
background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%);
|
||||
background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #3278b3;
|
||||
}
|
||||
.panel {
|
||||
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .05);
|
||||
}
|
||||
.panel-default > .panel-heading {
|
||||
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
|
||||
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.panel-primary > .panel-heading {
|
||||
background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%);
|
||||
background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.panel-success > .panel-heading {
|
||||
background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);
|
||||
background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.panel-info > .panel-heading {
|
||||
background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);
|
||||
background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.panel-warning > .panel-heading {
|
||||
background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);
|
||||
background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.panel-danger > .panel-heading {
|
||||
background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);
|
||||
background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
.well {
|
||||
background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
|
||||
background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #dcdcdc;
|
||||
-webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1);
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-theme.css.map */
|
File diff suppressed because one or more lines are too long
7
senpy/static/css/bootstrap-theme.min.css
vendored
7
senpy/static/css/bootstrap-theme.min.css
vendored
File diff suppressed because one or more lines are too long
5785
senpy/static/css/bootstrap.css
vendored
5785
senpy/static/css/bootstrap.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
10
senpy/static/css/bootstrap.min.css
vendored
10
senpy/static/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
1
senpy/static/css/bootstrap.min.css.map
Normal file
1
senpy/static/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
@ -130,7 +130,7 @@ textarea{
|
||||
|
||||
.tab-content {
|
||||
padding: 1em;
|
||||
border: 1px solid #ddd;
|
||||
/* border: 1px solid #ddd; */
|
||||
border-top: none;
|
||||
}
|
||||
#content-services, #content-resources {
|
||||
@ -144,9 +144,12 @@ textarea{
|
||||
.center-block {
|
||||
float:none;
|
||||
}
|
||||
#header {
|
||||
#header h3, #header h4, #header h5, #header h6 {
|
||||
font-family: 'Architects Daughter', cursive;
|
||||
}
|
||||
#header {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#results-div {
|
||||
/* background: white; */
|
||||
@ -196,7 +199,63 @@ textarea{
|
||||
|
||||
}
|
||||
|
||||
small pre {
|
||||
font-size: smaller;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
|
||||
/* NODES */
|
||||
.node {
|
||||
stroke: #fff;
|
||||
fill:#ddd;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
.link {
|
||||
fill: none;
|
||||
stroke: #999;
|
||||
stroke-opacity: .6;
|
||||
stroke-width: 1px;
|
||||
}
|
||||
marker {
|
||||
stroke: #999;
|
||||
fill:rgba(124,240,10,0);
|
||||
}
|
||||
.node-text {
|
||||
font: 11px sans-serif;
|
||||
fill:black;
|
||||
}
|
||||
.link-text {
|
||||
font: 9px sans-serif;
|
||||
fill:grey;
|
||||
}
|
||||
|
||||
svg{
|
||||
border:1px solid black;
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
.ribbon {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
pre {outline: 1px solid #ccc; padding: 1em; }
|
||||
.string { color: green; }
|
||||
.number { color: darkorange; }
|
||||
.boolean { color: blue; }
|
||||
.null { color: magenta; }
|
||||
.key { color: black; }
|
||||
|
||||
#basic_params {
|
||||
padding: 2em;
|
||||
}
|
||||
|
BIN
senpy/static/img/ribbon.png
Normal file
BIN
senpy/static/img/ribbon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
1951
senpy/static/js/bootstrap.js
vendored
1951
senpy/static/js/bootstrap.js
vendored
File diff suppressed because it is too large
Load Diff
11
senpy/static/js/bootstrap.min.js
vendored
11
senpy/static/js/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
1
senpy/static/js/bootstrap.min.js.map
Normal file
1
senpy/static/js/bootstrap.min.js.map
Normal file
File diff suppressed because one or more lines are too long
5
senpy/static/js/d3.min.js
vendored
Normal file
5
senpy/static/js/d3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -5,6 +5,7 @@ var plugins = [];
|
||||
var defaultPlugin = {};
|
||||
var gplugins = {};
|
||||
var pipeline = [];
|
||||
var converter = new showdown.Converter({simplifiedAutoLink: true});
|
||||
|
||||
function replaceURLWithHTMLLinks(text) {
|
||||
console.log('Text: ' + text);
|
||||
@ -21,10 +22,17 @@ function encodeHTML(text) {
|
||||
};
|
||||
|
||||
function hashchanged(){
|
||||
var hash = location.hash
|
||||
, hashPieces = hash.split('?');
|
||||
if( hashPieces[0].length > 0 ){
|
||||
activeTab = $('[href=' + hashPieces[0] + ']');
|
||||
console.log('Hash changed');
|
||||
var hash = location.hash,
|
||||
hashPieces = hash.split('?');
|
||||
loc = hashPieces[0]
|
||||
if( loc.length > 0 ){
|
||||
|
||||
if(loc[loc.length-1] == '.' ){
|
||||
loc = loc.slice(0, -1)
|
||||
}
|
||||
console.log(loc)
|
||||
activeTab = $('[href=' + loc + ']');
|
||||
activeTab && activeTab.tab('show');
|
||||
}
|
||||
}
|
||||
@ -52,18 +60,8 @@ function group_plugins(){
|
||||
}
|
||||
}
|
||||
|
||||
function get_parameters(){
|
||||
for (p in plugins){
|
||||
plugin = plugins[p];
|
||||
if (plugin["extra_params"]){
|
||||
plugins_params[plugin["name"]] = plugin["extra_params"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function draw_plugins_selection(){
|
||||
html="";
|
||||
group_plugins();
|
||||
for (g in gplugins){
|
||||
html += "<optgroup label=\""+g+"\">"
|
||||
for (r in gplugins[g]){
|
||||
@ -83,14 +81,39 @@ function draw_plugins_selection(){
|
||||
html+=">"+plugin["name"]+"</option>"
|
||||
|
||||
}
|
||||
html += "</optgroup>"
|
||||
}
|
||||
html += "</optgroup>"
|
||||
// Two elements with plugin class
|
||||
// One from the evaluate tab and another one from the analyse tab
|
||||
plugin_lists = document.getElementsByClassName('plugin')
|
||||
for (element in plugin_lists){
|
||||
plugin_lists[element].innerHTML = html;
|
||||
$('#plugins-select').html(html)
|
||||
draw_plugin_pipeline();
|
||||
}
|
||||
|
||||
function draw_plugins_eval_selection(){
|
||||
evaluable = JSON.parse($.ajax({type: "GET", url: "/api/plugins/?plugin_type=Evaluable" , async: false}).responseText).plugins;
|
||||
|
||||
html="";
|
||||
for (r in evaluable){
|
||||
plugin = evaluable[r]
|
||||
if (!plugin["name"]){
|
||||
console.log("No name for plugin ", plugin);
|
||||
continue;
|
||||
|
||||
}
|
||||
html+= "<option value=\""+plugin.name+"\" "
|
||||
if (plugin["name"] == defaultPlugin["name"]){
|
||||
html+= " selected=\"selected\""
|
||||
}
|
||||
if (!plugin["is_activated"]){
|
||||
html+= " disabled=\"disabled\" "
|
||||
}
|
||||
html+=">"+plugin["name"]+"</option>"
|
||||
|
||||
}
|
||||
|
||||
// Two elements with plugin class
|
||||
// One from the evaluate tab and another one from the analyse tab
|
||||
$('#plugins-eval').html(html)
|
||||
draw_plugin_pipeline();
|
||||
}
|
||||
|
||||
@ -116,19 +139,30 @@ function remove_plugin_pipeline(name){
|
||||
function draw_plugins_list(){
|
||||
var availablePlugins = document.getElementById('availablePlugins');
|
||||
|
||||
for(p in plugins){
|
||||
var pluginEntry = document.createElement('li');
|
||||
plugin = plugins[p];
|
||||
newHtml = ""
|
||||
if(plugin.url) {
|
||||
newHtml= "<a href="+plugin.url+">" + plugin.name + "</a>";
|
||||
}else {
|
||||
newHtml= plugin["name"];
|
||||
availablePlugins.innerHTML = '';
|
||||
html = ''
|
||||
|
||||
for (g in gplugins){
|
||||
for (r in gplugins[g]){
|
||||
p = gplugins[g][r];
|
||||
plugin = plugins[p];
|
||||
html += `<div class="card my-2">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title"> ${plugin.name} <span class="badge badge-success">${plugin.version}</span>`
|
||||
// if (typeof plugin.author !== 'undefined'){
|
||||
// html += ` <span class="badge badge-secondary">${plugin.author}</span>`
|
||||
// }
|
||||
// for (ptype in plugin['@type'] ){
|
||||
html += ` <span class="badge badge-secondary">${plugin['@type']}</span>`
|
||||
// }
|
||||
html += `</h4>
|
||||
<p class="card-text">${converter.makeHtml(plugin.description)}</p>
|
||||
</div>
|
||||
</div>`
|
||||
html += "</div>";
|
||||
}
|
||||
newHtml += ": " + replaceURLWithHTMLLinks(plugin.description);
|
||||
pluginEntry.innerHTML = newHtml;
|
||||
availablePlugins.appendChild(pluginEntry)
|
||||
}
|
||||
availablePlugins.innerHTML = html;
|
||||
}
|
||||
|
||||
function add_plugin_pipeline(){
|
||||
@ -149,16 +183,19 @@ function draw_datasets(){
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
var response = JSON.parse($.ajax({type: "GET", url: "/api/plugins/" , async: false}).responseText);
|
||||
var response = JSON.parse($.ajax({type: "GET", url: "/api/plugins/?verbose=1" , async: false}).responseText);
|
||||
defaultPlugin= JSON.parse($.ajax({type: "GET", url: "/api/plugins/default" , async: false}).responseText);
|
||||
|
||||
get_plugins(response);
|
||||
get_default_parameters();
|
||||
|
||||
draw_plugins_list();
|
||||
group_plugins();
|
||||
draw_plugins_selection();
|
||||
draw_plugins_eval_selection();
|
||||
draw_parameters();
|
||||
draw_plugins_list();
|
||||
draw_plugin_description();
|
||||
example_selected();
|
||||
|
||||
if (evaluation_enabled) {
|
||||
var response2 = JSON.parse($.ajax({type: "GET", url: "/api/datasets/" , async: false}).responseText);
|
||||
@ -166,19 +203,33 @@ $(document).ready(function() {
|
||||
draw_datasets();
|
||||
}
|
||||
|
||||
$(window).on('hashchange', hashchanged);
|
||||
hashchanged();
|
||||
// $(window).on('hashchange', hashchanged);
|
||||
// hashchanged();
|
||||
$('.tooltip-form').tooltip();
|
||||
|
||||
$('.nav-pills a').on('shown.bs.tab', function (e) {
|
||||
window.location.hash = e.target.hash + '.';
|
||||
})
|
||||
$('form').on("submit",function( event ) {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function get_default_parameters(){
|
||||
default_params = JSON.parse($.ajax({type: "GET", url: "/api?help=true" , async: false}).responseText).valid_parameters;
|
||||
function get_parameters(plugin, verbose){
|
||||
verbose = typeof verbose !== 'undefined' ? verbose : false;
|
||||
resp = JSON.parse($.ajax({type: "GET", url: "/api/"+plugin+"?help=true&verbose=" + verbose , async: false}).responseText)
|
||||
params = resp.valid_parameters;
|
||||
// Remove the parameters that are always added
|
||||
delete default_params["input"];
|
||||
delete default_params["algorithm"];
|
||||
delete default_params["help"];
|
||||
delete params["input"];
|
||||
delete params["algorithm"];
|
||||
delete params["outformat"];
|
||||
delete params["help"];
|
||||
return params
|
||||
}
|
||||
|
||||
function get_default_parameters() {
|
||||
default_params = get_parameters('default', true);
|
||||
}
|
||||
|
||||
function get_selected_plugin(){
|
||||
@ -186,8 +237,9 @@ function get_selected_plugin(){
|
||||
}
|
||||
|
||||
function draw_default_parameters(){
|
||||
var basic_params = document.getElementById("basic_params");
|
||||
basic_params.innerHTML = params_div(default_params);
|
||||
bp = $('#basic_params');
|
||||
console.log(bp)
|
||||
bp.html(params_div(default_params));
|
||||
}
|
||||
|
||||
function update_params(params, plug){
|
||||
@ -200,7 +252,10 @@ function update_params(params, plug){
|
||||
|
||||
function draw_extra_parameters(){
|
||||
var plugin = get_selected_plugin();
|
||||
get_parameters();
|
||||
if (typeof plugins_params[plugin] === 'undefined' ){
|
||||
params = get_parameters(plugin, false);
|
||||
plugins_params[plugin] = params;
|
||||
}
|
||||
|
||||
var extra_params = document.getElementById("extra_params");
|
||||
var params = {};
|
||||
@ -218,17 +273,15 @@ function draw_parameters(){
|
||||
|
||||
|
||||
function add_default_params(){
|
||||
var html = "";
|
||||
var html = "<form>";
|
||||
// html += '<a href="#basic_params" class="btn btn-info" data-toggle="collapse">Basic API parameters</a>';
|
||||
html += '<span id="basic_params" class="panel-collapse collapse">';
|
||||
html += '<ul class="list-group">'
|
||||
html += params_div(default_params);
|
||||
html += '</span>';
|
||||
html += '</form>'
|
||||
return html;
|
||||
}
|
||||
|
||||
function params_div(params){
|
||||
var html = '<div class="container-fluid">';
|
||||
var html = '';
|
||||
if (Object.keys(params).length === 0) {
|
||||
html += '<p class="text text-muted text-center">This plugin does not take any extra parameters</p>';
|
||||
}
|
||||
@ -267,13 +320,14 @@ function params_div(params){
|
||||
html+='</div>';
|
||||
html+='<div class="row">';
|
||||
if ('description' in param){
|
||||
html += '<p class="form-text sm-sm-12 text-muted text-center">' + param.description + '</p>';
|
||||
|
||||
html += '<small class="form-text sm-sm-12 text-muted">'
|
||||
html += converter.makeHtml(param.description) + '</small>';
|
||||
|
||||
}
|
||||
html+='</div>';
|
||||
html+='</div>';
|
||||
}
|
||||
html+='</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
@ -325,19 +379,18 @@ function get_pipeline_arg(){
|
||||
}
|
||||
|
||||
|
||||
function load_JSON(){
|
||||
function get_result(outformat, cb, eb) {
|
||||
url = "/api";
|
||||
var container = document.getElementById('results');
|
||||
var rawcontainer = document.getElementById("jsonraw");
|
||||
rawcontainer.innerHTML = '';
|
||||
container.innerHTML = '';
|
||||
$('.results').html('');
|
||||
|
||||
var plugin = get_pipeline_arg();
|
||||
$(".loading").addClass("loader");
|
||||
$("#preview").hide();
|
||||
|
||||
|
||||
var input = encodeURIComponent(document.getElementById("input").value);
|
||||
url += "?algo="+plugin+"&i="+input
|
||||
|
||||
url += "?algo="+plugin+"&i="+input+"&outformat="+outformat;
|
||||
|
||||
params = get_form_parameters();
|
||||
|
||||
@ -345,26 +398,47 @@ function load_JSON(){
|
||||
url += add_param(key, params[key]);
|
||||
}
|
||||
|
||||
$.ajax({type: "GET", url: url}).always(function(response){
|
||||
document.getElementById("results-div").style.display = 'block';
|
||||
if(typeof response=="object") {
|
||||
var options = {
|
||||
mode: 'view'
|
||||
};
|
||||
var editor = new JSONEditor(container, options, response);
|
||||
editor.expandAll();
|
||||
$('#results-div a[href="#viewer"]').click();
|
||||
response = JSON.stringify(response, null, 4);
|
||||
} else {
|
||||
console.log("Got turtle?");
|
||||
$('#results-div a[href="#raw"]').click();
|
||||
}
|
||||
|
||||
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
|
||||
document.getElementById("input_request").innerHTML = "<a href='"+url+"'>"+url+"</a>"
|
||||
|
||||
return $.ajax({type: "GET", url: url}).done(function(){
|
||||
$(".loading").removeClass("loader");
|
||||
$("#preview").show();
|
||||
})
|
||||
}
|
||||
|
||||
function load_results(outformat){
|
||||
|
||||
if (typeof outformat === 'undefined') {
|
||||
active = $('#results-container .tab-pane.active').get(0);
|
||||
console.log(active.id);
|
||||
outformat = active.id
|
||||
if (outformat == 'viewer') {
|
||||
outformat = 'json-ld';
|
||||
}
|
||||
}
|
||||
|
||||
get_result(outformat).always(function(response, txt, request){
|
||||
console.log('outformat is', outformat)
|
||||
document.getElementById("results-div").style.display = 'block';
|
||||
container = $('#results-'+outformat).get(0);
|
||||
raw = '';
|
||||
|
||||
try {
|
||||
raw = replaceURLWithHTMLLinks(response)
|
||||
}catch(error){
|
||||
response = JSON.stringify(response, null, 1);
|
||||
raw = syntaxHighlight(response)
|
||||
}
|
||||
console.log('highlighted');
|
||||
container.innerHTML = replaceURLWithHTMLLinks(raw);
|
||||
|
||||
document.getElementById("input_request").innerHTML = "<a href='"+url+"'>"+url+"</a>"
|
||||
console.log("Request failed", request);
|
||||
$(".loading").removeClass("loader");
|
||||
$("#preview").show();
|
||||
}).fail(function(data) {
|
||||
if (data.readyState == 0) {
|
||||
$('#results-'+outformat).html('');
|
||||
alert( "The server is not responding. Make sure it is still running." );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -385,9 +459,14 @@ function create_body_metrics(evaluations){
|
||||
var new_tbody = document.createElement('tbody')
|
||||
var metric_html = ""
|
||||
for (var eval in evaluations){
|
||||
metric_html += "<tr><th>"+evaluations[eval].evaluates+"</th><th>"+evaluations[eval].evaluatesOn+"</th>";
|
||||
metric_html += "<tr><td>"+evaluations[eval].evaluates+"</td><td>"+evaluations[eval].evaluatesOn+"</td>";
|
||||
for (var metric in evaluations[eval].metrics){
|
||||
metric_html += "<th>"+parseFloat(evaluations[eval].metrics[metric].value.toFixed(4))+"</th>";
|
||||
var value = "Not available";
|
||||
try {
|
||||
value = parseFloat(evaluations[eval].metrics[metric].value.toFixed(4));
|
||||
}catch(err){
|
||||
}
|
||||
metric_html += "<td>"+value+"</td>";
|
||||
}
|
||||
metric_html += "</tr>";
|
||||
}
|
||||
@ -396,6 +475,7 @@ function create_body_metrics(evaluations){
|
||||
}
|
||||
|
||||
function evaluate_JSON(){
|
||||
$(".loading").addClass("loader");
|
||||
|
||||
url = "/api/evaluate";
|
||||
|
||||
@ -405,56 +485,64 @@ function evaluate_JSON(){
|
||||
|
||||
rawcontainer.innerHTML = "";
|
||||
container.innerHTML = "";
|
||||
$("#evaluate-div").hide();
|
||||
|
||||
var plugin = document.getElementsByClassName("plugin")[0].options[document.getElementsByClassName("plugin")[0].selectedIndex].value;
|
||||
var plugin = $("#plugins-eval option:selected").val();
|
||||
|
||||
get_datasets_from_checkbox();
|
||||
|
||||
url += "?algo="+plugin+"&dataset="+datasets
|
||||
|
||||
$('#doevaluate').attr("disabled", true);
|
||||
$.ajax({type: "GET", url: url, dataType: 'json'}).always(function(resp) {
|
||||
$.ajax({type: "GET", url: url, dataType: 'text'}).always(function(response) {
|
||||
$('#doevaluate').attr("disabled", false);
|
||||
response = resp.responseText;
|
||||
|
||||
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
|
||||
|
||||
document.getElementById("input_request_eval").innerHTML = "<a href='"+url+"'>"+url+"</a>"
|
||||
document.getElementById("evaluate-div").style.display = 'block';
|
||||
|
||||
$('#evaluate-div a[href="#evaluate-raw"]').click();
|
||||
|
||||
try {
|
||||
response = JSON.parse(response);
|
||||
var options = {
|
||||
mode: 'view'
|
||||
};
|
||||
|
||||
//Control the single response results
|
||||
if (!(Array.isArray(response.evaluations))){
|
||||
response.evaluations = [response.evaluations]
|
||||
}
|
||||
|
||||
new_tbody = create_body_metrics(response.evaluations)
|
||||
table.replaceChild(new_tbody, table.lastElementChild)
|
||||
|
||||
var editor = new JSONEditor(container, options, response);
|
||||
editor.expandAll();
|
||||
// $('#results-div a[href="#viewer"]').tab('show');
|
||||
$('#evaluate-div a[href="#evaluate-table"]').click();
|
||||
// location.hash = 'raw';
|
||||
|
||||
|
||||
js = JSON.parse(response);
|
||||
rawcontainer.innerHTML = replaceURLWithHTMLLinks(response);
|
||||
}
|
||||
catch(err){
|
||||
console.log("Error decoding JSON (got turtle?)");
|
||||
console.log("Error decoding JSON");
|
||||
if (typeof(response.responseText) !== 'undefined') {
|
||||
rawcontainer.innerHTML = response.responseText;
|
||||
}
|
||||
else {
|
||||
rawcontainer.innerHTML = response;
|
||||
}
|
||||
$(".loading").removeClass("loader");
|
||||
$('#evaluate-div a[href="#evaluate-raw"]').click();
|
||||
// location.hash = 'raw';
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof(js['senpy:evaluations']) === 'undefined') {
|
||||
alert('Could not evaluate on that dataset');
|
||||
} else {
|
||||
//Control the single response results
|
||||
if (!(Array.isArray(js['senpy:evaluations']))){
|
||||
js['senpy:evaluations'] = [js['senpy:evaluations']]
|
||||
rawcontainer.innerHtml = response;
|
||||
}
|
||||
new_tbody = create_body_metrics(js['senpy:evaluations'])
|
||||
table.replaceChild(new_tbody, table.lastElementChild)
|
||||
$('#evaluate-div a[href="#evaluate-table"]').click();
|
||||
// location.hash = 'raw';
|
||||
$("#evaluate-div").show();
|
||||
}
|
||||
|
||||
$(".loading").removeClass("loader");
|
||||
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
function draw_plugin_description(){
|
||||
var plugin = plugins[get_selected_plugin()];
|
||||
$("#plugdescription").text(plugin.description);
|
||||
$("#plugdescription").html(converter.makeHtml(plugin.description));
|
||||
console.log(plugin);
|
||||
}
|
||||
|
||||
@ -462,3 +550,32 @@ function plugin_selected(){
|
||||
draw_extra_parameters();
|
||||
draw_plugin_description();
|
||||
}
|
||||
|
||||
function example_selected(){
|
||||
console.log('changing text');
|
||||
var text = $('#examples option:selected').data("text");
|
||||
$('#input').val(text);
|
||||
console.log('done');
|
||||
}
|
||||
|
||||
function syntaxHighlight(json) {
|
||||
if (typeof json != 'string') {
|
||||
json = JSON.stringify(json, undefined, 1);
|
||||
}
|
||||
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
|
||||
var cls = 'number';
|
||||
if (/^"/.test(match)) {
|
||||
if (/:$/.test(match)) {
|
||||
cls = 'key';
|
||||
} else {
|
||||
cls = 'string';
|
||||
}
|
||||
} else if (/true|false/.test(match)) {
|
||||
cls = 'boolean';
|
||||
} else if (/null/.test(match)) {
|
||||
cls = 'null';
|
||||
}
|
||||
return '<span class="' + cls + '">' + match + '</span>';
|
||||
});
|
||||
}
|
||||
|
223
senpy/static/js/nodes.js
Normal file
223
senpy/static/js/nodes.js
Normal file
@ -0,0 +1,223 @@
|
||||
ns = {
|
||||
'http://www.gsi.dit.upm.es/ontologies/marl/ns#': 'marl',
|
||||
'http://www.gsi.dit.upm.es/ontologies/onyx/ns#': 'onyx',
|
||||
'http://www.gsi.dit.upm.es/ontologies/senpy/ns#': 'onyx',
|
||||
'http://www.gsi.upm.es/onto/senpy/ns#': 'senpy',
|
||||
'http://www.w3.org/ns/prov#': 'prov',
|
||||
'http://persistence.uni-leipzig.org/nlp2rdf/ontologies/nif-core#': 'nif'
|
||||
}
|
||||
|
||||
mappings = {
|
||||
'http://www.w3.org/1999/02/22-rdf-syntax-ns#type': 'a',
|
||||
}
|
||||
|
||||
function load_graph(){
|
||||
|
||||
function filterNodesById(nodes,id){
|
||||
return nodes.filter(function(n) { return n.id === id; });
|
||||
}
|
||||
|
||||
function filterNodesByType(nodes,value){
|
||||
return nodes.filter(function(n) { return n.type === value; });
|
||||
}
|
||||
|
||||
function triplesToGraph(triples){
|
||||
|
||||
svg.html("");
|
||||
//Graph
|
||||
var graph={nodes:[], links:[], triples: []};
|
||||
|
||||
triples = triples.filter(t=>t!=null).map(t => {
|
||||
return t.map(e =>{
|
||||
ids = e.match(/^\<(.*)\>/)
|
||||
if (! ids ) {
|
||||
return e
|
||||
|
||||
}
|
||||
id = ids[1]
|
||||
for (ix in ns) {
|
||||
id = id.replace(ix, ns[ix] + ':')
|
||||
}
|
||||
if (! (id in mappings)) {
|
||||
console.log(id, id in mappings)
|
||||
return id
|
||||
}
|
||||
return mappings[id]
|
||||
})
|
||||
})
|
||||
|
||||
//Initial Graph from triples
|
||||
triples.forEach(function(triple){
|
||||
if (triple == null) {
|
||||
return
|
||||
}
|
||||
var subjId = triple[0];
|
||||
var predId = triple[1];
|
||||
var objId = triple[2];
|
||||
|
||||
|
||||
|
||||
var subjNode = filterNodesById(graph.nodes, subjId)[0];
|
||||
var objNode = filterNodesById(graph.nodes, objId)[0];
|
||||
|
||||
if(subjNode==null){
|
||||
subjNode = {id:subjId, label:subjId, weight:1, type:"node"};
|
||||
graph.nodes.push(subjNode);
|
||||
}
|
||||
|
||||
if(objNode==null){
|
||||
objNode = {id:objId, label:objId, weight:1, type:"node"};
|
||||
graph.nodes.push(objNode);
|
||||
}
|
||||
|
||||
var predNode = {id:predId, label:predId, weight:1, type:"pred"} ;
|
||||
graph.nodes.push(predNode);
|
||||
|
||||
var blankLabel = "";
|
||||
|
||||
graph.links.push({source:subjNode, target:predNode, predicate:blankLabel, weight:1});
|
||||
graph.links.push({source:predNode, target:objNode, predicate:blankLabel, weight:1});
|
||||
graph.triples.push({s:subjNode, p:predNode, o:objNode});
|
||||
|
||||
|
||||
});
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
|
||||
function update(graph){
|
||||
|
||||
|
||||
// ==================== Add Marker ====================
|
||||
svg.append("svg:defs").selectAll("marker")
|
||||
.data(["end"])
|
||||
.enter().append("svg:marker")
|
||||
.attr("id", String)
|
||||
.attr("viewBox", "0 -5 10 10")
|
||||
.attr("refX", 30)
|
||||
.attr("refY", -0.5)
|
||||
.attr("markerWidth", 6)
|
||||
.attr("markerHeight", 6)
|
||||
.attr("orient", "auto")
|
||||
.append("svg:polyline")
|
||||
.attr("points", "0,-5 10,0 0,5")
|
||||
;
|
||||
|
||||
|
||||
// ==================== Add Links ====================
|
||||
var links = svg.selectAll(".link")
|
||||
.data(graph.triples)
|
||||
.enter()
|
||||
.append("path")
|
||||
.attr("marker-end", "url(#end)")
|
||||
.attr("class", "link")
|
||||
;
|
||||
;//links
|
||||
|
||||
// ==================== Add Link Names =====================
|
||||
var linkTexts = svg.selectAll(".link-text")
|
||||
.data(graph.triples)
|
||||
.enter()
|
||||
.append("text")
|
||||
.attr("class", "link-text")
|
||||
.text( function (d) { return d.p.label; })
|
||||
;
|
||||
|
||||
|
||||
//linkTexts.append("title")
|
||||
// .text(function(d) { return d.predicate; });
|
||||
|
||||
// ==================== Add Link Names =====================
|
||||
var nodeTexts = svg.selectAll(".node-text")
|
||||
.data(filterNodesByType(graph.nodes, "node"))
|
||||
.enter()
|
||||
.append("text")
|
||||
.attr("class", "node-text")
|
||||
.text( function (d) { return d.label; })
|
||||
;
|
||||
//nodeTexts.append("title")
|
||||
// .text(function(d) { return d.label; });
|
||||
|
||||
// ==================== Add Node =====================
|
||||
var nodes = svg.selectAll(".node")
|
||||
.data(filterNodesByType(graph.nodes, "node"))
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("class", "node")
|
||||
.attr("r",8)
|
||||
.call(force.drag)
|
||||
;//nodes
|
||||
|
||||
// ==================== Add Predicate =====================
|
||||
/*var preds = svg.selectAll(".node")
|
||||
.data(graph.preds)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("class", "node")
|
||||
.attr("r",1)
|
||||
//.call(force.drag)*/
|
||||
;//nodes
|
||||
|
||||
// ==================== Force ====================
|
||||
force.on("tick", function() {
|
||||
nodes
|
||||
.attr("cx", function(d){ return d.x; })
|
||||
.attr("cy", function(d){ return d.y; })
|
||||
;
|
||||
|
||||
links
|
||||
.attr("d", function(d) {
|
||||
return "M" + d.s.x + "," + d.s.y
|
||||
+ "S" + d.p.x + "," + d.p.y
|
||||
+ " " + d.o.x + "," + d.o.y;
|
||||
})
|
||||
;
|
||||
|
||||
nodeTexts
|
||||
.attr("x", function(d) { return d.x + 12 ; })
|
||||
.attr("y", function(d) { return d.y + 3; })
|
||||
;
|
||||
|
||||
|
||||
linkTexts
|
||||
.attr("x", function(d) { return 4 + (d.s.x + d.p.x + d.o.x)/3 ; })
|
||||
.attr("y", function(d) { return 4 + (d.s.y + d.p.y + d.o.y)/3 ; })
|
||||
; });
|
||||
|
||||
// ==================== Run ====================
|
||||
force
|
||||
.nodes(graph.nodes)
|
||||
.links(graph.links)
|
||||
.charge(-500)
|
||||
.linkDistance(50)
|
||||
.start()
|
||||
;
|
||||
}
|
||||
|
||||
get_result('ntriples').done(resp => {
|
||||
triples = resp.split('\n').map(line => line.match(/[^" ][^ ]*|\"[^"]+\"[^ ]*/g))
|
||||
|
||||
console.log(triples);
|
||||
|
||||
|
||||
var graph = triplesToGraph(triples);
|
||||
console.log(graph);
|
||||
|
||||
update(graph);
|
||||
}).fail(resp => {
|
||||
alert('Could not get a response.');
|
||||
});
|
||||
|
||||
d3.select('#svg-body').selectAll("*").remove();
|
||||
var svg = d3.select("#svg-body").append("svg")
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
;
|
||||
|
||||
var height = 600;
|
||||
var width = $('.tab-content').width();
|
||||
|
||||
console.log('Graph with', width, height);
|
||||
var force = d3.layout.force().size([width, height]);
|
||||
}
|
3
senpy/static/js/showdown.min.js
vendored
Normal file
3
senpy/static/js/showdown.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,264 +1,319 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
<title>Playground {{version}}</title>
|
||||
<head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
|
||||
<title>Playground {{version}}</title>
|
||||
|
||||
</head>
|
||||
<script>
|
||||
this.evaluation_enabled = {% if evaluation %}true{%else %}false{%endif%};
|
||||
</script>
|
||||
<script src="static/js/jquery-2.1.1.min.js" ></script>
|
||||
<!--<script src="jquery.autosize.min.js"></script>-->
|
||||
<link rel="stylesheet" href="static/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="static/css/main.css">
|
||||
<link rel="stylesheet" href="static/font-awesome-4.1.0/css/font-awesome.min.css">
|
||||
<link href="static/css/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
<script>
|
||||
this.evaluation_enabled = {% if evaluation %}true{%else %}false{%endif%};
|
||||
</script>
|
||||
<script src="static/js/jquery-2.1.1.min.js" ></script>
|
||||
<script src="static/js/d3.min.js" ></script>
|
||||
<!--<script src="jquery.autosize.min.js"></script>-->
|
||||
<link rel="stylesheet" href="static/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="static/css/main.css">
|
||||
<link rel="stylesheet" href="static/font-awesome-4.1.0/css/font-awesome.min.css">
|
||||
|
||||
|
||||
|
||||
<script src="static/js/bootstrap.min.js"></script>
|
||||
<script src="static/js/jsoneditor.js"></script>
|
||||
<script src="static/js/main.js"></script>
|
||||
<script src="static/js/bootstrap.min.js"></script>
|
||||
<script src="static/js/showdown.min.js"></script>
|
||||
<script src="static/js/main.js"></script>
|
||||
<script src="static/js/nodes.js"></script>
|
||||
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div id="header">
|
||||
<h3 id="header-title">
|
||||
<a href="https://github.com/gsi-upm/senpy" target="_blank">
|
||||
<img id="header-logo" class="imsg-responsive" src="static/img/header.png"/></a> Playground
|
||||
</h3>
|
||||
<h4>v{{ version}}</h4>
|
||||
</div>
|
||||
<body>
|
||||
<nav id="header" class="navbar navbar-default sticky-top">
|
||||
<div class="container">
|
||||
|
||||
<h3 id="header-title" class="p-2">
|
||||
<a href="https://github.com/gsi-upm/senpy" target="_blank">
|
||||
<img id="header-logo" class="imsg-responsive" src="static/img/header.png"/></a> Playground
|
||||
</h3>
|
||||
<span class="nav nav-pills p-2" id="nav-pills" role="pill-list">
|
||||
<a class="nav-item nav-link" data-toggle="pill" id="nav-about" role="pill" aria-controls="about" href="#about" aria-selected="false" >About</a>
|
||||
<a class="nav-item nav-link" data-toggle="pill" id="nav-plugins" role="pill" aria-controls="plugins" href="#plugins" aria-selected="false">Plugins</a>
|
||||
<a class="nav-item nav-link active" data-toggle="pill" id="nav-test" role="pill" aria-controls="test" href="#test" aria-selected="true">Test it</a>
|
||||
{% if evaluation %}
|
||||
<a class="nav-item nav-link" data-toggle="pill" id="nav-evaluate" role="pill" aria-controls="evaluate" href="#evaluate" aria-selected="false">Evaluate Plugins</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
<h6 class="p-2 ml-auto">v{{ version}}</h6>
|
||||
</div>
|
||||
</nav>
|
||||
<div id="content" class="container">
|
||||
<a href="https://github.com/gsi-upm/senpy" target="_blank"><img width="149" height="149" src="static/img/ribbon.png" class="ribbon" alt="Fork me on GitHub" data-recalc-dims="1"></a>
|
||||
<div class="tab-content">
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" ><a class="active" href="#about">About</a></li>
|
||||
<li role="presentation"class="active"><a class="active" href="#test">Test it</a></li>
|
||||
{% if evaluation %}
|
||||
<li role="presentation"><a class="active" href="#evaluate">Evaluate Plugins</a></li>
|
||||
{% endif %}
|
||||
<div class="tab-pane" role="tabpanel" aria-labelledby="nav-about" id="about">
|
||||
|
||||
</ul>
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<h2>About Senpy</h2>
|
||||
<p>Senpy is a framework to build semantic sentiment and emotion analysis services. It leverages a mix of web and semantic technologies, such as JSON-LD, RDFlib and Flask.</p>
|
||||
<p>Senpy makes it easy to develop and publish your own analysis algorithms (plugins in senpy terms).
|
||||
</p>
|
||||
<p>
|
||||
This website is the senpy Playground, which allows you to test the instance of senpy in this server. It provides a user-friendly interface to the functions exposed by the senpy API.
|
||||
</p>
|
||||
<p>
|
||||
Once you get comfortable with the parameters and results, you are encouraged to issue your own requests to the API endpoint. You can find examples of API URL's when you try out a plugin with the "Analyse!" button on the "Test it" tab.
|
||||
</p>
|
||||
<p>
|
||||
These are some of the things you can do with the API:
|
||||
<ul>
|
||||
<li>List all available plugins: <a href="/api/plugins">/api/plugins</a></li>
|
||||
<li>Get information about the default plugin: <a href="/api/plugins/default">/api/plugins/default</a></li>
|
||||
<li>List all available datasets: <a href="/api/datasets">/api/datasets</a></li>
|
||||
<li>Download the JSON-LD context used: <a href="/api/contexts/Results.jsonld">/api/contexts/Results.jsonld</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane" id="about">
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<h2>About Senpy</h2>
|
||||
<p>Senpy is a framework to build semantic sentiment and emotion analysis services. It does so by using a mix of web and semantic technologies, such as JSON-LD, RDFlib and Flask.</p>
|
||||
<p>Senpy makes it easy to develop and publish your own analysis algorithms (plugins in senpy terms).
|
||||
</p>
|
||||
<p>
|
||||
This website is the senpy Playground, which allows you to test the instance of senpy in this server. It provides a user-friendly interface to the functions exposed by the senpy API.
|
||||
</p>
|
||||
<p>
|
||||
Once you get comfortable with the parameters and results, you are encouraged to issue your own requests to the API endpoint. You can find examples of API URL's when you try out a plugin with the "Analyse!" button on the "Test it" tab.
|
||||
</p>
|
||||
<p>
|
||||
These are some of the things you can do with the API:
|
||||
<ul>
|
||||
<li>List all available plugins: <a href="/api/plugins">/api/plugins</a></li>
|
||||
<li>Get information about the default plugin: <a href="/api/plugins/default">/api/plugins/default</a></li>
|
||||
<li>List all available datasets: <a href="/api/datasets">/api/datasets</a></li>
|
||||
<li>Download the JSON-LD context used: <a href="/api/contexts/Results.jsonld">/api/contexts/Results.jsonld</a></li>
|
||||
</ul>
|
||||
|
||||
</p>
|
||||
<p>Senpy is a research project. If you use it in your research, please cite:
|
||||
<pre>
|
||||
<div class="card my-2">
|
||||
<div id="plugin_selection" class="card-body">
|
||||
<h6 class="card-title">
|
||||
Senpy is a research project. If you use it in your research, please cite:
|
||||
</h6>
|
||||
<pre>
|
||||
Senpy: A Pragmatic Linked Sentiment Analysis Framework.
|
||||
Sánchez-Rada, J. F., Iglesias, C. A., Corcuera, I., & Araque, Ó.
|
||||
In Data Science and Advanced Analytics (DSAA),
|
||||
2016 IEEE International Conference on (pp. 735-742). IEEE.
|
||||
</pre>
|
||||
</p>
|
||||
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<a href="http://senpy.readthedocs.io">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title"><i class="fa fa-book"></i> If you are new to senpy, you might want to read senpy's documentation</h6>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="http://www.github.com/gsi-upm/senpy">
|
||||
<div class="card my-2">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title"><i class="fa fa-sign-in"></i> If you like senpy, feel free star it on GitHub</h6>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 ">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
Available Plugins
|
||||
<div class="tab-pane" role="tabpanel" aria-labelledby="nav-plugins" id="plugins">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h5>The following plugins are available in this instance:</h5>
|
||||
<div id="availablePlugins" class="card-columns"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body"><ul id=availablePlugins></ul></div>
|
||||
</div>
|
||||
<a href="http://senpy.readthedocs.io">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><i class="fa fa-book"></i> If you are new to senpy, you might want to read senpy's documentation</div>
|
||||
</div>
|
||||
</a>
|
||||
<a href="http://www.github.com/gsi-upm/senpy">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><i class="fa fa-sign-in"></i> Feel free to follow us on GitHub</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div role="tabpanel" aria-labelledby="nav-test" class="tab-pane active" id="test">
|
||||
<div class="card my-2">
|
||||
<div class="card-body">
|
||||
<form id="form" class="container" onsubmit="load_results();" accept-charset="utf-8">
|
||||
<label>Enter the text you want to analyze or select one of the pre-defined examples:</label>
|
||||
<div>
|
||||
<select id="examples" name="examples" onchange="example_selected()">
|
||||
<option data-text="Who knew NLP and text preprocessing could be so easy with python? #DataScience #NLP">
|
||||
Regular Tweet</option>
|
||||
<option data-text="Russia attacked the 2016 election to help Trump. His campaign signaled Moscow it was fine with that, and Trump also lied about the attack and helped Putin get away with it. And most Republicans and conservatives don’t give a damn. This is sad and troubling.">
|
||||
Political Tweet</option>
|
||||
<option data-text='The bus was traveling from Florida to New York with 57 people aboard when it swerved "like a roller coaster" and tumbled "five or six times" off the left side of a Virginia interstate, killing two passengers and injuring others aboard.'>
|
||||
Excerpt from a news article</option>
|
||||
</select>
|
||||
</div>
|
||||
<div><textarea id="input" class="boxsizingBorder" rows="5" name="i"></textarea>
|
||||
</div>
|
||||
<!-- PARAMETERS -->
|
||||
<div id="parameters">
|
||||
<div class="card my-2">
|
||||
<div class="card-header">
|
||||
<h5>
|
||||
Select the plugin.
|
||||
</h5>
|
||||
</div>
|
||||
<div id="plugin_selection" class="card-body">
|
||||
<span id="pipeline"></span>
|
||||
<select id="plugins-select" name="plugins" class="plugin" onchange="plugin_selected()">
|
||||
</select>
|
||||
<span onclick="add_plugin_pipeline()"><span class="btn"><i class="fa fa-plus" title="Add more plugins to the pipeline. Processing order is left to right. i.e. the results of the leftmost plugin will be used as input for the second leftmost, and so on."></i></span></span>
|
||||
<span class="help-block " id="plugdescription"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card my-2">
|
||||
<div class="card-header">
|
||||
<h5>
|
||||
Plugin extra parameters
|
||||
</h5>
|
||||
</div>
|
||||
<div id="extra_params" class="card-body">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="card my-2">
|
||||
<div class="card-body">
|
||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#parametersModal">
|
||||
Change advanced parameters
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- MODAL -->
|
||||
<div class="modal fade" id="parametersModal" tabindex="-1" role="dialog" aria-labelledby="modalTitle" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modalTitle">Advanced API parameters</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="basic_params" class="modal-body">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END MODAL -->
|
||||
<!-- END PARAMETERS -->
|
||||
|
||||
<button id="preview" class="btn btn-lg btn-primary" onclick="load_results()">Analyse</button>
|
||||
<div id="loading-results" class="loading"></div>
|
||||
<!--<button id="visualise" name="type" type="button">Visualise!</button>-->
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<span id="input_request"></span>
|
||||
<div id="results-div">
|
||||
<ul class="nav nav-pills nav-header-pills" role="tablist">
|
||||
<li role="presentation" class="nav-item active"><a data-toggle="pill" class="active nav-link" href="#json-ld" onclick='load_results("json-ld")'>JSON-LD</a></li>
|
||||
<li role="presentation nav-item" ><a class="nav-link" data-toggle="pill" href="#turtle" onclick='load_results("turtle")'>Turtle</a></li>
|
||||
<li role="presentation nav-item" ><a class="nav-link" data-toggle="pill" href="#ntriples" onclick='load_results("ntriples")'>N-Triples</a></li>
|
||||
<li role="presentation nav-item" ><a class="nav-link" data-toggle="pill" href="#graph" onclick='load_graph()'>Graph</a></li>
|
||||
</ul>
|
||||
<div class="tab-content" id="results-container">
|
||||
|
||||
<div role="tabpanel" aria-labelledby="" class="tab-pane active" id="json-ld">
|
||||
<div>
|
||||
<pre id="results-json-ld" class="results"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" role="tabpanel" aria-labelledby="" id="turtle">
|
||||
<div>
|
||||
<pre id="results-turtle" class="results"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" role="tabpanel" aria-labelledby="" id="ntriples">
|
||||
<div>
|
||||
<pre id="results-ntriples" class="results"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" role="tabpanel" aria-labelledby="" id="graph">
|
||||
<svg id='svg-body'></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if evaluation %}
|
||||
|
||||
<div class="tab-pane" role="tabpanel" aria-labelledby="nav-evaluate" id="evaluate">
|
||||
<div class="card my-2">
|
||||
<div class="card-body">
|
||||
|
||||
<form id="form" class="container" onsubmit="" accept-charset="utf-8">
|
||||
<div>
|
||||
<p>Automatically evaluate the classification performance of your plugin in several public datasets, and compare it with other plugins.</p>
|
||||
<p>The datasets will be automatically downloaded if they are not already available locally. Depending on the size of the dataset and the speed of the plugin, the evaluation may take a long time.</p>
|
||||
<label>Select the plugin:</label>
|
||||
<select id="plugins-eval" name="plugins-eval" class=plugin onchange="draw_extra_parameters()">
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Select the datasets:</label>
|
||||
<div id="datasets" name="datasets" >
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button id="doevaluate" class="btn btn-lg btn-primary" onclick="evaluate_JSON()">Evaluate Plugin</button>
|
||||
<!--<button id="visualise" name="type" type="button">Visualise!</button>-->
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div id="loading-results" class="loading"></div>
|
||||
<span id="input_request_eval"></span>
|
||||
<div id="evaluate-div">
|
||||
<ul class="nav nav-pills" role="tablist">
|
||||
|
||||
<li role="presentation nav-item" ><a class="nav-link" data-toggle="pill" href="#evaluate-raw" onclick=''>Raw</a></li>
|
||||
<li role="presentation nav-item" ><a class="nav-link" data-toggle="pill" href="#evaluate-table" onclick=''>Table</a></li>
|
||||
</ul>
|
||||
<div class="tab-content" id="evaluate-container">
|
||||
<div role="tabpanel" aria-labelledby="" class="tab-pane active" id="evaluate-viewer">
|
||||
<div>
|
||||
<pre id="results_eval" class="results_eval"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" role="tabpanel" aria-labelledby="" id="evaluate-raw">
|
||||
<div>
|
||||
<pre id="jsonraw_eval" class="results_eval"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" role="tabpanel" aria-labelledby="" id="evaluate-table">
|
||||
<table id="eval_table" class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Plugin</th>
|
||||
<th>Dataset</th>
|
||||
<th>Accuracy</th>
|
||||
<th>Precision_macro</th>
|
||||
<th>Recall_macro</th>
|
||||
<th>F1_macro</th>
|
||||
<th>F1_weighted</th>
|
||||
<th>F1_micro</th>
|
||||
<th>F1</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-pane active" id="test">
|
||||
<div class="well">
|
||||
<form id="form" class="container" onsubmit="return getPlugins();" accept-charset="utf-8">
|
||||
<div><textarea id="input" class="boxsizingBorder" rows="5" name="i">This text makes me sad.
|
||||
whilst this text makes me happy and surprised at the same time.
|
||||
I cannot believe it!</textarea>
|
||||
<div id="footer">
|
||||
<div class="container">
|
||||
<div id="site-info">
|
||||
<p>
|
||||
This development has been partially funded by the European Union through the<a href="http://mixedemotions-project.eu/"><span style="text-transform:uppercase;">MixedEmotions project</span></a>(project number H2020 655632), as part of the RIA ICT 15 Big data and Open Data Innovation and take-up programme.
|
||||
</p>
|
||||
</div>
|
||||
<!-- PARAMETERS -->
|
||||
<div class="panel-group" id="parameters">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
Select the plugin.
|
||||
</h4>
|
||||
</div>
|
||||
<div id="plugin_selection" class="panel-collapse panel-body">
|
||||
<span id="pipeline"></span>
|
||||
<select name="plugins" class="plugin" onchange="plugin_selected()">
|
||||
</select>
|
||||
<span onclick="add_plugin_pipeline()"><span class="btn"><i class="fa fa-plus" title="Add more plugins to the pipeline. Processing order is left to right. i.e. the results of the leftmost plugin will be used as input for the second leftmost, and so on."></i></span></span>
|
||||
<label class="help-block " id="plugdescription"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<a data-toggle="collapse" class="deco-none collapsed" href="#basic_params">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fa fa-chevron-right pull-left expandicon"></i>
|
||||
<i class="fa fa-chevron-down pull-left collapseicon"></i>
|
||||
Basic API parameters
|
||||
</h4>
|
||||
</div>
|
||||
</a>
|
||||
<div id="basic_params" class="panel-collapse collapse panel-body">
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<a data-toggle="collapse" class="deco-none" href="#extra_params">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fa fa-chevron-right pull-left expandicon"></i>
|
||||
<i class="fa fa-chevron-down pull-left collapseicon"></i>
|
||||
Plugin extra parameters
|
||||
</h4>
|
||||
</div>
|
||||
</a>
|
||||
<div id="extra_params" class="panel-collapse collapse in panel-body">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END PARAMETERS -->
|
||||
|
||||
<a id="preview" class="btn btn-lg btn-primary" onclick="load_JSON()">Analyse!</a>
|
||||
<div id="loading-results" class="loading"></div>
|
||||
<!--<button id="visualise" name="type" type="button">Visualise!</button>-->
|
||||
</form>
|
||||
</div>
|
||||
<span id="input_request"></span>
|
||||
<div id="results-div">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a data-toggle="tab" class="active" href="#viewer">Viewer</a></li>
|
||||
<li role="presentation"><a data-toggle="tab" class="active" href="#raw">Raw</a></li>
|
||||
</ul>
|
||||
<div class="tab-content" id="results-container">
|
||||
|
||||
<div class="tab-pane active" id="viewer">
|
||||
<div id="content">
|
||||
<pre id="results" class="results"></pre>
|
||||
</div>
|
||||
<div id="site-logos">
|
||||
<a href="http://www.gsi.dit.upm.es" target="_blank"><img id="mixedemotions-logo"src="static/img/me.png"/></a>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="raw">
|
||||
<div id="content">
|
||||
<pre id="jsonraw" class="results"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if evaluation %}
|
||||
|
||||
<div class="tab-pane" id="evaluate">
|
||||
<div class="well">
|
||||
<form id="form" class="container" onsubmit="return getPlugins();" accept-charset="utf-8">
|
||||
<div>
|
||||
<label>Select the plugin:</label>
|
||||
<select id="plugins-eval" name="plugins-eval" class=plugin onchange="draw_extra_parameters()">
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Select the datasets:</label>
|
||||
<div id="datasets" name="datasets" >
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<a id="doevaluate" class="btn btn-lg btn-primary" onclick="evaluate_JSON()">Evaluate Plugin!</a>
|
||||
<!--<button id="visualise" name="type" type="button">Visualise!</button>-->
|
||||
</form>
|
||||
</div>
|
||||
<span id="input_request_eval"></span>
|
||||
<div id="evaluate-div">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a data-toggle="tab" class="active" href="#evaluate-viewer">Viewer</a></li>
|
||||
<li role="presentation"><a data-toggle="tab" class="active" href="#evaluate-raw">Raw</a></li>
|
||||
<li role="presentation"><a data-toggle="tab" class="active" href="#evaluate-table">Table</a></li>
|
||||
</ul>
|
||||
<div class="tab-content" id="evaluate-container">
|
||||
|
||||
<div class="tab-pane active" id="evaluate-viewer">
|
||||
<div id="content">
|
||||
<pre id="results_eval" class="results_eval"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane" id="evaluate-raw">
|
||||
<div id="content">
|
||||
<pre id="jsonraw_eval" class="results_eval"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="evaluate-table">
|
||||
<table id="eval_table" class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Plugin</th>
|
||||
<th>Dataset</th>
|
||||
<th>Accuracy</th>
|
||||
<th>Precision_macro</th>
|
||||
<th>Recall_macro</th>
|
||||
<th>F1_macro</th>
|
||||
<th>F1_weighted</th>
|
||||
<th>F1_micro</th>
|
||||
<th>F1</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<a href="http://www.gsi.dit.upm.es" target="_blank"><img class="center-block" src="static/img/gsi.png"/> </a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div id="footer">
|
||||
<div class="container">
|
||||
<div id="site-info">
|
||||
<p>
|
||||
This development has been partially funded by the European Union through the<a href="http://mixedemotions-project.eu/"><span style="text-transform:uppercase;">MixedEmotions project</span></a>(project number H2020 655632), as part of the RIA ICT 15 Big data and Open Data Innovation and take-up programme.
|
||||
</p>
|
||||
</div>
|
||||
<div id="site-logos">
|
||||
<a href="http://www.gsi.dit.upm.es" target="_blank"><img id="mixedemotions-logo"src="static/img/me.png"/></a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<link href='http://fonts.googleapis.com/css?family=Architects+Daughter' rel='stylesheet' type='text/css'>
|
||||
</body>
|
||||
<link href='http://fonts.googleapis.com/css?family=Architects+Daughter' rel='stylesheet' type='text/css'>
|
||||
</html>
|
||||
|
@ -77,6 +77,7 @@ def easy_test(plugin_list=None, debug=True):
|
||||
logger.info('Loading classes from {}'.format(__main__))
|
||||
from . import plugins
|
||||
plugin_list = plugins.from_module(__main__)
|
||||
plugin_list = list(plugin_list)
|
||||
for plug in plugin_list:
|
||||
plug.test()
|
||||
plug.log.info('My tests passed!')
|
||||
@ -87,7 +88,7 @@ def easy_test(plugin_list=None, debug=True):
|
||||
pdb.post_mortem()
|
||||
|
||||
|
||||
def easy(host='0.0.0.0', port=5000, debug=True, **kwargs):
|
||||
def easy(host='0.0.0.0', port=5000, debug=False, **kwargs):
|
||||
'''
|
||||
Run a server with a specific plugin.
|
||||
'''
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user