Skip to content

Commit

Permalink
Add Recast.AI parser
Browse files Browse the repository at this point in the history
  • Loading branch information
FabioRosado committed Nov 15, 2017
1 parent 7993215 commit 9f4fc0f
Show file tree
Hide file tree
Showing 7 changed files with 496 additions and 1 deletion.
115 changes: 115 additions & 0 deletions docs/matchers/recast.ai.md
@@ -0,0 +1,115 @@
# wit.ai Matcher

[Recast.AI](https://recast.ai/) is an NLP API for matching strings to [intents](https://recast.ai/docs/intent). Intents are created on the Recast.AI website.

## [Example 1](#example1)

```python
from opsdroid.matchers import match_recastai

@match_recastai('greetings')
async def hello(opsdroid, config, message):
"""Replies to user when any 'greetings'
intent is returned by Recast.AI
"""
await message.respond("Hello there!")
```

The above skill would be called on any intent which has a name of `'greetings'`.

## Example 2

```python
from opsdroid.matchers import match_recastai

@match_recastai('ask-joke')
async def my_skill(opsdroid, config, message):
"""Returns a joke if asked by the user"""
await message.respond('What do you call a bear with no teeth? -- A gummy bear!')
```

The above skill would be called on any intent which has a name of `'ask-joke'`.


## Creating a Recast.AI bot
You need to [register](https://recast.ai/signup) on Recast.AI and create a bot in order to use Recast.AI with opsdroid.

You can find a quick getting started with the Recast.AI guide [here](https://recast.ai/docs/create-your-bot).

## Configuring opsdroid

In order to enable Recast.AI skills, you must specify an `access-token` for your bot in the parsers section of the opsdroid configuration file.
You can find this `access-token` in the settings of your bot under the name: `'Request access token'`.

You can also set a `min-score` option to tell opsdroid to ignore any matches which score less than a given number between 0 and 1. The default for this is 0 which will match all messages.

```yaml

parsers:
- name: recastai
access-token: 85769fjoso084jd
min-score: 0.8
```

## Message object additional parameters

### `message.recastai`

An http response object which has been returned by the Recast.AI API. This allows you to access any information from the matched intent including other entities, intents, values, etc.


## Example Skill

```python
from opsdroid.matchers import match_recastai

import json


@match_recastai('ask-feeling')
async def dumpResponse(opsdroid, config, message):
print(json.dumps(message.witai))
```

### Return Value on "How are you?"

The example skill will print the following on the message "how are you?".

```json
{
"results":
{
"uuid": "cab86e23-caaf-4131-9b83-a564887203da",
"source": "how are you?",
"intents": [
{
"slug": "ask-feeling",
"confidence": 0.99
}
],
"act": "wh-query",
"type": "desc:manner",
"sentiment": "neutral",
"entities":
{
"pronoun": [
{
"person": 2,
"number": "singular",
"gender": "unknown",
"raw": "you",
"confidence": 0.99
}
]
},
"language": "en",
"processing_language": "en",
"version": "2.10.1",
"timestamp": "2017-11-15T11:50:51.478057+00:00",
"status": 200},
"message": "Requests rendered with success"
}
}
```


1 change: 1 addition & 0 deletions mkdocs.yml
Expand Up @@ -15,6 +15,7 @@ pages:
- Regular Expressions: 'matchers/regex.md'
- Dialogflow (Api.ai): 'matchers/dialogflow.md'
- LUIS.AI: 'matchers/luis.ai.md'
- Recast.AI: 'matchers/recast.ai.md'
- wit.ai: 'matchers/wit.ai.md'
- Crontab: 'matchers/crontab.md'
- Webhook: 'matchers/webhook.md'
Expand Down
9 changes: 9 additions & 0 deletions opsdroid/core.py
Expand Up @@ -15,6 +15,7 @@
from opsdroid.parsers.regex import parse_regex
from opsdroid.parsers.dialogflow import parse_dialogflow
from opsdroid.parsers.luisai import parse_luisai
from opsdroid.parsers.recastai import parse_recastai
from opsdroid.parsers.witai import parse_witai
from opsdroid.parsers.crontab import parse_crontab
from opsdroid.const import DEFAULT_CONFIG_PATH
Expand Down Expand Up @@ -242,6 +243,14 @@ async def get_ranked_skills(self, message):
skills = skills + \
await parse_luisai(self, message, luisai[0])

recastai = [p for p in parsers if p["name"] == "recastai"]
if len(recastai) == 1 and \
("enabled" not in recastai[0] or
recastai[0]["enabled"] is not False):
_LOGGER.debug("Checking Recast.AI...")
skills = skills + \
await parse_recastai(self, message, recastai[0])

witai = [p for p in parsers if p["name"] == "witai"]
if len(witai) == 1 and \
("enabled" not in witai[0] or
Expand Down
12 changes: 12 additions & 0 deletions opsdroid/matchers.py
Expand Up @@ -89,6 +89,18 @@ def matcher(func):
return matcher


def match_recastai(intent):
"""Return recastai intent match decorator."""
def matcher(func):
"""Add decorated function to skills list for recastai matching."""
opsdroid = get_opsdroid()
opsdroid.skills.append({"recastai_intent": intent, "skill": func,
"config":
opsdroid.loader.current_import_config})
return func
return matcher


def match_witai(intent):
"""Return witai intent match decorator."""
def matcher(func):
Expand Down
75 changes: 75 additions & 0 deletions opsdroid/parsers/recastai.py
@@ -0,0 +1,75 @@
"""A helper function for parsing and executing Recast.AI skills."""
import logging
import json

import aiohttp


_LOGGER = logging.getLogger(__name__)


async def call_recastai(message, config):
"""Call the recastai api and return the response."""
async with aiohttp.ClientSession() as session:
payload = {
"language": "en",
"text": message.text
}
headers = {
"Authorization": "Token " + config['access-token'],
"Content-Type": "application/json"
}
resp = await session.post("https://api.recast.ai/v2/request",
data=json.dumps(payload),
headers=headers)
result = await resp.json()
_LOGGER.info("Recastai response - %s", json.dumps(result))

return result


async def parse_recastai(opsdroid, message, config):
"""Parse a message against all recastai intents."""
matched_skills = []
if 'access-token' in config:
try:
result = await call_recastai(message, config)
except aiohttp.ClientOSError:
_LOGGER.error("No response from Recast.AI, check your network.")
return matched_skills

if result['results'] is None:
_LOGGER.error("Recast.AI error - %s", result["message"])
return matched_skills
elif not result["results"]["intents"]:
_LOGGER.error("Recast.AI error - No intent found "
"for the message %s", str(message.text))
return matched_skills

# try:
# confidence = result["results"]["intents"][0]["confidence"]
# except (KeyError, IndexError):
# confidence = 0.0
if "min-score" in config and \
result["results"]["intents"][0]["confidence"] < \
config["min-score"]:
_LOGGER.debug("Recast.AI score lower than min-score")
return matched_skills

if result:
for skill in opsdroid.skills:
if "recastai_intent" in skill:
if (skill["recastai_intent"] in
result["results"]["intents"][0]["slug"]):
message.recastai = result
_LOGGER.debug("Matched against skill %s",
skill["config"]["name"])

matched_skills.append({
"score":
result["results"]["intents"][0]["confidence"],
"skill": skill["skill"],
"config": skill["config"],
"message": message
})
return matched_skills
17 changes: 16 additions & 1 deletion tests/test_core.py
Expand Up @@ -10,7 +10,8 @@
from opsdroid.message import Message
from opsdroid.connector import Connector
from opsdroid.matchers import (match_regex, match_dialogflow_action,
match_luisai_intent, match_witai)
match_luisai_intent, match_recastai,
match_witai)


class TestCore(unittest.TestCase):
Expand Down Expand Up @@ -228,6 +229,20 @@ async def test_parse_luisai(self):
for task in tasks:
await task

async def test_parse_recastai(self):
with OpsDroid() as opsdroid:
opsdroid.config["parsers"] = [{"name": "recastai"}]
recastai_intent = ""
skill = amock.CoroutineMock()
mock_connector = Connector({})
match_recastai(recastai_intent)(skill)
message = Message("Hello", "user", "default", mock_connector)
with amock.patch('opsdroid.parsers.recastai.parse_recastai'):
tasks = await opsdroid.parse(message)
self.assertEqual(len(tasks), 1)
for task in tasks:
await task

async def test_parse_witai(self):
with OpsDroid() as opsdroid:
opsdroid.config["parsers"] = [{"name": "witai"}]
Expand Down

0 comments on commit 9f4fc0f

Please sign in to comment.