はじめに
Android で Dialogflow を使いたい!というのはよくある話だと思う。
さて、作ってみよう!とまずは SDK が無いか調べてみる。
https://cloud.google.com/dialogflow/docs/reference/libraries/java
おっと、Android はサポートしていないだって。
みんなどうやって開発してるんだろ?と思って調べてみると、api.ai なるものを発見!
なんだって。
Dialogflow API V2 を使いたいなら client library を使え?
サポート外って書いてるじゃん…。
はてさて、REST API 使うしかないのか。
けど、SDK は音声入力ストリームもできるようで、是非とも使いたい!
そこで、Java client library がどこまで動くのか検証してみました。
今回は DetectIntent が動くか確認してみます。
別途、音声入力ストリームも試す予定です。
(追記)
音声入力ストリーム編もアップしました!
https://qiita.com/r-hashioka/items/a9d0ca0704ef8dd739a2
ただ、今回記事ありきで書いてるので結構端折ってます。
対象者
Dialogflow の機能説明などは省きます。
はじめましての人は公式ドキュメントなどを見てから読むといいかもしれません。
環境
言語:Kotlin
端末:Pixcel3a (Android 10)
Dialogflow の 事前準備
Default Welcome Intent
today's_weather
説明
- 挨拶をすると、
Default Welcome Intent
に入り、コマンド受付状態に入ります。 - 「今日の天気は?」と問いかけると「今日の天気は晴れです」と応答がある。といった流れです。
Let's Programming!!!
まずはライブラリ import から
dependencies に下記2つを追加してください。
Dialogflow のライブラリだけではダメだったようです。
implementation 'com.google.cloud:google-cloud-dialogflow:0.105.0-alpha'
// https://mvnrepository.com/artifact/io.grpc/grpc-okhttp
// GCP サービスとコネクション貼るときに使用
implementation 'io.grpc:grpc-okhttp:1.25.0'
Manifest に PERMISSION 追加
- Dialogflow に接続するので INTERNET を追加
- 音声入力ストリームも見越して、RECORD_AUDIO も追加
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
credentials.json を配置
まずはアカウントキー IAM の サービスアカウント の「Dialogflow Integrations」から鍵を作成し、JSONをダウンロード
細かい手順は省きます。
下記を参考にダウンロードしてください。
https://cloud.google.com/iam/docs/creating-managing-service-account-keys?hl=ja#iam-service-account-keys-create-console
https://www.magellanic-clouds.com/blocks/guide/create-gcp-service-account-key/res/raw
にcredentials.json
を配置
※※注意※※
Credential を端末に持たせるのはいいことではありません。
今回は検証でとりあえず、とやっているだけですので、対策必須です!
layout を作成
各 UI の機能は下記の通りです。
- 「こんにちは」ボタンを押すと、Dialogflow に「こんにちは」を送信
- 「今日の天気」ボタンを押すと、Dialogflow に「今日の天気は?」を送信
- Dialogflow からの応答を Hello World! の TextView に表示
- 「RESET」ボタンを押すと、contexts を削除します。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/greetingBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="こんにちは"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/weatherBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="今日の天気"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/greetingBtn" />
<Button
android:id="@+id/resetBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="RESET"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/weatherBtn" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity にボタンイベント追加
※この時点ではエラー発生します。
package jp.hashioka.ryo.dialogflowsample
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import jp.hashioka.ryo.dialogflowsample.dialogflow.DetectIntent
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val detectIntent = DetectIntent(this)
findViewById<Button>(R.id.greetingBtn).setOnClickListener {
findViewById<TextView>(R.id.textView).text = detectIntent.send("こんにちは")
}
findViewById<Button>(R.id.weatherBtn).setOnClickListener {
findViewById<TextView>(R.id.textView).text = detectIntent.send("今日の天気は?")
}
findViewById<Button>(R.id.resetBtn).setOnClickListener {
detectIntent.resetContexts()
findViewById<TextView>(R.id.textView).text = "-"
}
}
companion object {
const val TAG = "MainActivity"
}
}
DetectIntent クラス作成
dialogflow パッケージを作成し、DetectIntent.kt ファイルを作成します。
package jp.hashioka.ryo.dialogflowsample.dialogflow
import android.content.Context
import android.util.Log
import com.google.api.gax.core.FixedCredentialsProvider
import com.google.auth.oauth2.GoogleCredentials
import com.google.cloud.dialogflow.v2.*
import jp.hashioka.ryo.dialogflowsample.R
/**
* Dialogflow の detectIntent に関するクラス
*/
class DetectIntent (
context: Context
) {
companion object {
private const val TAG = "DetectIntent"
const val PROJECT_ID = "voice-recognition-trial-261200"
const val LANGUAGE_CODE = "ja" // TODO: Dialogflow の言語コードはグローバル対応するときに設定ファイルで管理
val SCOPE = listOf("https://www.googleapis.com/auth/cloud-platform")
/**
* セッションを取得する。
* TODO : Dialogflow のセッションはクライアント毎にユニークとなるよう処理を記述する。
*/
private fun getSession() : String {
return "hogehoge"
}
}
private val sessionsClient : SessionsClient
private val contextClient : ContextsClient
init {
// 認証情報セット
val credentials = GoogleCredentials
.fromStream(context.resources.openRawResource(R.raw.credentials))
.createScoped(SCOPE)
sessionsClient = createSessions(credentials)
contextClient = createContexts(credentials)
}
/**
* SessionClient を作成する。
*/
private fun createSessions(credentials: GoogleCredentials): SessionsClient {
val sessionsSetting =
SessionsSettings.newBuilder()
.setCredentialsProvider(FixedCredentialsProvider.create(credentials))
.build()
return SessionsClient.create(sessionsSetting)
}
/**
* ContextsClient を作成する。
*/
private fun createContexts(credentials: GoogleCredentials) : ContextsClient {
val contextsSettings =
ContextsSettings.newBuilder()
.setCredentialsProvider(FixedCredentialsProvider.create(credentials))
.build()
return ContextsClient.create(contextsSettings)
}
/**
* detectIntent を実行し、その結果を返却
* 指定されたテキストを送信するだけ。
*/
fun send(text: String) : String {
val request = DetectIntentRequest.newBuilder()
.setQueryInput(
QueryInput.newBuilder()
.setText(
TextInput.newBuilder()
.setText(text)
.setLanguageCode(LANGUAGE_CODE)
)
.build())
.setSession(SessionName.format(PROJECT_ID, getSession()))
.build()
val res = sessionsClient.detectIntent(request)
Log.d(TAG, "response result : ${res.queryResult}")
return res.queryResult.fulfillmentText
}
/**
* detectIntent を実行し、その結果を返却
* context 指定可能
*/
fun send(text: String, contexts: List<String>) : String {
val queryParametersBuilder = QueryParameters.newBuilder()
contexts.forEach {
queryParametersBuilder
.addContexts(
com.google.cloud.dialogflow.v2.Context.newBuilder()
.setName(ContextName.format(PROJECT_ID, getSession(), it))
.setLifespanCount(5) // TODO: context の Lifespan を動的にする。
.build()
)
}
// Dialogflow に投げるテキスト、コンテキストなどセット
val request = DetectIntentRequest.newBuilder()
.setQueryParams(queryParametersBuilder.build())
.setQueryInput(
QueryInput.newBuilder()
.setText(
TextInput.newBuilder()
.setText(text)
.setLanguageCode(LANGUAGE_CODE)
)
.build())
.setSession(SessionName.format(PROJECT_ID, getSession()))
.build()
val res = sessionsClient.detectIntent(request)
Log.d(TAG, "response result : ${res.queryResult}")
return res.queryResult.fulfillmentText
}
/**
* context をリセットする。
*/
fun resetContexts() {
contextClient.deleteAllContexts(SessionName.format(PROJECT_ID, getSession()))
}
}
おお!動いた!
さいごに
Android でも DetectIntent はできました。
多分、REST API の機能くらいはできるんでは?と思ってます。
結構走り書きでしたが、最後まで読んでいただきありがとうございます。
(追記)
ソースコードを GitHub に公開しました。(音声入力ストリームも含む)
https://github.com/ryohashioka/DialogflowSampleForAndroid