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 httpsthen we will need-d:sslflag 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
authorizationusinggetWitAIRequestHeaders - Prepares the full URL using the
WIT_API_HOSTand the queryparamssent - Based on the method
HttpGetorHttpPostit'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,coordsfor 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.