Day 19: Wit.AI client
Nim client for wit.ai to Easily create text or voice based bots that humans can chat with on their preferred messaging platform. It helps to reduce expressions into entity/trait
e.g in your wit.ai project you define entity like VM
(virtual machine) and trait to be something like create
, stop
and when you send an expression like new virtual machine
or fresh vm
, wit.ai helps to reduce it to entity vm
and trait create
What to expect
let tok = getEnv("WIT_ACCESS_TOKEN", "")
if tok == "":
echo "Make sure to set WIT_ACCESS_TOKEN variable"
quit 1
var inp = ""
var w = newWit(tok)
while true:
echo "Enter your query or q to quit > "
inp = stdin.readLine()
if inp == "q":
quit 0
else:
echo w.message(inp)
Enter your query or q to quit >
new vm
{"_text":"new vm","entities":{"vm":[{"confidence":0.97072907352305,"value":"create"}]},"msg_id":"1N6CURN7qaJaSKXSK"}
Enter your query or q to quit >
new machine
{"_text":"new machine","entities":{"vm":[{"confidence":0.90071815565634,"value":"create"}]},"msg_id":"1t8dOpkPbAP6SgW49"}
Enter your query or q to quit >
new docker
{"_text":"new docker","entities":{"container":[{"confidence":0.98475238333984,"value":"create"}]},"msg_id":"1l7ocY7MVWBfUijsm"}
Enter your query or q to quit >
stop machine
{"_text":"stop machine","entities":{"vm":[{"confidence":0.66323929848545,"value":"stop"}]},"msg_id":"1ygXLjnQbEt4lVMyS"}
Enter your query or q to quit >
show my coins
{"_text":"show my coins","entities":{"wallet":[{"confidence":0.75480999601329,"value":"show"}]},"msg_id":"1SdYOY60xXdMvUG7b"}
Enter your query or q to quit >
view coins
{"_text":"view coins","entities":{"wallet":[{"confidence":0.5975926583378,"value":"show"}]},"msg_id":"1HZ3YlfLlr31JlbKZ"}
Enter your query or q to quit >
Speech
echo w.speech("/home/striky/startnewvm.wav", {"Content-Type": "audio/wav"}.toTable)
{
"_text" : "start new the m is",
"entities" : {
"vm" : [ {
"confidence" : 0.54805678200202,
"value" : "create"
} ]
},
"msg_id" : "1jHMTJGHEAFh8LHFS"
}
Implementation
imports
import strformat, tables, json, strutils, sequtils, hashes, net, asyncdispatch,
asyncnet, os, strutils, parseutils, deques, options, net
import json
import logging
import httpclient
import uri
var L = newConsoleLogger()
addHandler(L)
Here we import utilities we are going to use like string formatters, tables, json, http client .. etc and prepare default logger.
Crafting wit.ai API
let WIT_API_HOST = getEnv("WIT_URL", "https://api.wit.ai")
let WIT_API_VERSION = getEnv("WIT_API_VERSION", "20160516")
let DEFAULT_MAX_STEPS = 5
To work with wit.ai
API you will need to generate an API token.
WIT_API_HOST
: base URL for wit.ai APInotice it's https
then we will need-d:ssl
flag in compile phase.WIT_API_VERSION
: API version in wit.ai
We will be interested in /message
and /speech
endpoints in wit.ai API
Adding authorization to HTTP Headers
proc getWitAIRequestHeaders*(accessToken: string): HttpHeaders =
result = newHttpHeaders({
"authorization": "Bearer " & accessToken,
"accept": "application/vnd.wit." & WIT_API_VERSION & "+json"
})
To authorize our requests against wit.ai we need to add authorization
header.
Encoding params helper
proc encodeQueryStringTable(qsTable: Table[string, string]): string =
result = ""
if qsTable.len == 0:
return result
result = "?"
var first = true
for k, v in qsTable.pairs:
if not first:
result &= "&"
result &= fmt"{k}={encodeUrl(v)}"
first = false
echo $result
return result
A helper to encode key, value pairs into a query string `?key=val
Let's get to the client
Here we define the interesting parts to interact with wit.ai
type WitException* = object of Exception
Generic Exception to use
type Wit* = ref object of RootObj
accessToken*: string
client*: HttpClient
proc newWit(accessToken: string): Wit =
var w = new Wit
w.accessToken = accessToken
w.client = newHttpClient()
result = w
the entry point for our Wit.AI
client. the client Wit
keeps track of
accessToken
: to access the APIclient
: http client to use underneath
proc newRequest(this: Wit, meth = HttpGet, path: string, params: Table[string,
string], body = "", headers: Table[string, string]): string =
let fullUrl = WIT_API_HOST & path & encodeQueryStringTable(params)
this.client.headers = getWitAIRequestHeaders(this.accessToken)
if headers.len > 0:
for k, v in headers:
this.client.headers[k] = v
var resp: Response
if body == "":
resp = this.client.request(fullUrl, httpMethod = meth)
else:
resp = this.client.request(fullUrl, httpMethod = meth, body = body)
if resp.code != 200.HttpCode:
raise newException(WitException, (fmt"[-] {resp.code}: {resp.body} "))
result = resp.body
Generic helper to format/build wit.ai
requests. It does the following
- Prepares the headers with
authorization
usinggetWitAIRequestHeaders
- Prepares the full URL using the
WIT_API_HOST
and the queryparams
sent - Based on the method
HttpGet
orHttpPost
it'll issue the request and raises if response's status code is not200
- Returns the response body
/message endpoint
According to the docs of wit.ai only q
param is required.
Definition
GET https://api.wit.ai/message
Example request with single outcome
$ curl -XGET 'https://api.wit.ai/message?v=20170307&q=how%20many%20people%20between%20Tuesday%20and%20Friday' \
-H 'Authorization: Bearer $TOKEN'
Example response
{
"msg_id": "387b8515-0c1d-42a9-aa80-e68b66b66c27",
"_text": "how many people between Tuesday and Friday",
"entities": {
"metric": [ {
"metadata": "{'code': 324}",
"value": "metric_visitor",
"confidence": 0.9231
} ],
"datetime": [
{
"confidence": 0.954105,
"values": [
{
"to": {
"value": "2018-12-22T00:00:00.000-08:00",
"grain": "day"
},
"from": {
"value": "2018-12-18T00:00:00.000-08:00",
"grain": "day"
},
"type": "interval"
},
{
"to": {
"value": "2018-12-29T00:00:00.000-08:00",
"grain": "day"
},
"from": {
"value": "2018-12-25T00:00:00.000-08:00",
"grain": "day"
},
"type": "interval"
},
{
"to": {
"value": "2019-01-05T00:00:00.000-08:00",
"grain": "day"
},
"from": {
"value": "2019-01-01T00:00:00.000-08:00",
"grain": "day"
},
"type": "interval"
}
],
"to": {
"value": "2018-12-22T00:00:00.000-08:00",
"grain": "day"
},
"from": {
"value": "2018-12-18T00:00:00.000-08:00",
"grain": "day"
},
"type": "interval"
}
]
}
}
proc message*(this: Wit, msg: string, context: ref Table[string, string] = nil,
n = "", verbose = ""): string =
var params = initTable[string, string]()
if n != "":
params["n"] = n
if verbose != "":
params["verbose"] = verbose
if msg != "":
params["q"] = msg
if not context.isNil and context.len > 0:
var ctxNode = %* {}
for k, v in context.pairs:
ctxNode[k] = %*v
params["context"] = ( %* ctxNode).pretty()
return this.newRequest(HttpGet, path = "/message", params, "", initTable[
string, string]())
here we will allow msg
as the expression we want to check in wit.ai, and adding some extra params for more close mapping to the official API like context
, verbose
, n
msg
: User’s query. Length must be > 0 and < 280verbose
: A flag to get auxiliary information about entities, like the location within the sentence.n
: The maximum number of n-best trait entities you want to get back. The default is 1, and the maximum is 8context
: Context is key in natural language. For instance, at the same absolute instant, “today” will be resolved to a different value depending on the timezone of the user. (can containlocale
,timezone
,coords
for coordinates)
/speech endpoint
proc speech*(this: Wit, audioFilePath: string, headers: Table[string, string],
context: ref Table[string, string] = nil, n = "", verbose = ""): string =
var params = initTable[string, string]()
if n != "":
params["n"] = n
if verbose != "":
params["verbose"] = verbose
if not context.isNil and context.len > 0:
var ctxNode = %* {}
for k, v in context.pairs:
ctxNode[k] = %*v
params["context"] = ( %* ctxNode).pretty()
let body = readFile(audioFilePath)
return this.newRequest(HttpPost, path = "/speech", params, body, headers)
almost the same as /message
endpoint except we send audioFile content in body
same as /message
, but we will send an audio file.
Thanks
The complete sources can be found at nim-witai. Please feel free to contribute by opening PR or issue on the repo.