苹果有Siri,百度有小度,小米有小爱,而且后来竟然又出了个小兵,总之类似的智能聊天机器人是越来越多了。面对这样智能的机器人,我们似乎只能是体验者。想想底层的算法就让人头疼,它到底是怎么识别出一句话的意思的?又是怎么实现智能回复的?难道这就是传说中的机器学习、神经网络?不不不,其实这叫图灵机器人。也许底层算法真的很难很复杂,但如果你想实现一个自己的机器人,其实一点也不难。
今天就手把手教大家实现一个属于自己的智能聊天机器人。
首先我们百度图灵机器人,进入官网,网址为http://www.tuling123.com/
点击右上角的小头像便可以进入控制台了
来到控制台我们就可以根据自己的需求创建机器人了,过程没什么难度,这里有一点需要注意,当我们创建完成之后点击设置
会进入到如下界面
注意这个密钥开关,不要打开,不要打开,不要打开!!!否则在开发过程中会报40001:加密方式错误。
如果大家自学能力比较强可以直接看文档接入,不懂的地方再来借鉴一下。文档地址为
https://www.kancloud.cn/turing/www-tuling123-com/718227这里也有一点需要注意,接口地址只允许post请求访问,我们直接用浏览器是无法访问的。所以不要以为浏览器不能访问接口就不能用了。
必要的准备工作已经完成了,下面进入开发阶段。
整个聊天机器人的核心思想就是将用户发送的信息通过post请求访问图灵机器人接口,然后解析接口返回的数据,将有用的回复信息提取出来显示到界面上。
首先创建一个工具类叫做HttpUtils用来处理网络请求。用原生的HttpUrlConnection也可以完成,但远远不如OkHttp用起来方便,因此我们选择OkHttp来进行网络请求,打开app的build.gradle文件,在dependencies中加入
implementation 'com.squareup.okhttp3:okhttp:3.12.1'//用于处理网络请求
implementation 'com.google.code.gson:gson:2.8.5'//用于解析json数据
implementation 'de.hdodenhof:circleimageview:3.0.0'//用于处理圆形头像
不要忘了声明权限
<uses-permission android:name="android.permission.INTERNET"/>
然后实现我们的网络请求方法
/**
* 使用OkHttp自身回调请求数据
* @param msg
* @param callback
* enqueue()方法中会自动开启线程
*/
public static void sendOkHttpRequest(String msg, okhttp3.Callback callback) {
OkHttpClient client = new OkHttpClient();
final String json = "{" +
"\"reqType\":0," +
" \"perception\": {" +
" \"inputText\": {" +
" \"text\": \"" + msg + "\"" +
" }," +
" \"inputImage\": {" +
" \"url\": \"\"" +
" }," +
" \"selfInfo\": {" +
" \"location\": {" +
" \"city\": \"天津\"," +
" \"province\": \"天津\",\n" +
" \"street\": \"天津理工大学\"\n" +
" }" +
" }" +
" }," +
" \"userInfo\": {" +
" \"apiKey\": \"" + API_KEY + "\"," +
" \"userId\": \"" + "572780350" + "\"" +
" }" +
"}";
RequestBody body = RequestBody.create(JSON,json);
Request request = new Request.Builder()
.url(URL)
.post(body)
.build();
//注意这里用的是enqueue()方法,此方法会在内部自动开启一个线程
client.newCall(request).enqueue(callback);
}
至于我们为什么这样构建请求体呢,那是因为官方文档中给出了请求示例。 我们只需要将inputText下面的text内容替换为用户输入的信息,并将userInfo下面的apikey替换为我们创建机器人时得到的apikey就可以了。此外还需要将userId替换为一个长度小于等于32位的字符串,用于标识用户,这里我用了自己的QQ号。
可以看到里面有很多请求信息,其中某些是必须要填写 的,还有一些是不必须的,具体大家可以查看文档。
还有一点需要注意的是网络请求需要在子线程中进行,而我们并没有开启子线程,而是在方法中接收了一个 okhttp3.Callback类型的参数。而且也没有像往常一样使用client.newCall(request).execute();方法,而是调用了
client.newCall(request).enqueue(callback);方法。其实enqueue()方法内部已经自动帮我们开启了子线程,我们只需要在调用的时候实现okhttp3提供的callback接口就可以轻松处理返回的数据了。
当然,如果你不习惯使用okhttp3提供的callback接口的话,可以实现自己的接口,并在自己定义的接口中处理返回数据,就像下面这样。
/**
* 自己构造回调方法请求数据,测试时使用
* @param msg
* @param listener
* 因为是网络请求,因此需要开启线程
*/
public static void doRequest(String msg, final HttpCallbackListener listener) {
final String json = "{" +
"\"reqType\":0," +
" \"perception\": {" +
" \"inputText\": {" +
" \"text\": \"" + msg + "\"" +
" }," +
" \"inputImage\": {" +
" \"url\": \"\"" +
" }," +
" \"selfInfo\": {" +
" \"location\": {" +
" \"city\": \"天津\"," +
" \"province\": \"天津\",\n" +
" \"street\": \"天津理工大学\"\n" +
" }" +
" }" +
" }," +
" \"userInfo\": {" +
" \"apiKey\": \"" + API_KEY + "\"," +
" \"userId\": \"" + "572780350" + "\"" +
" }" +
"}";
new Thread(new Runnable() {
@Override
public void run() {
String strResult = "";
Response responseData = null;
OkHttpClient client = new OkHttpClient();
RequestBody body = RequestBody.create(JSON,json);
Request request = new Request.Builder()
.url(URL)
.post(body)
.build();
try {
responseData = client.newCall(request).execute();
String response = responseData.body().string();
Log.d("xxx","请求成功" + responseData);
strResult = parJson(response);
if(listener != null) {
listener.finish(strResult);
}
} catch (IOException e) {
e.printStackTrace();
if(listener != null) {
listener.onError(e);
}
} finally {
responseData.body().close();
}
}
}).start();
}
其中的HttpCallbackListener接口定义如下,分别处理请求成功的信息,和请求失败的情况。
public interface HttpCallbackListener {
void finish(String response);
void onError(Exception e);
}
然后就可以在主活动中对接口进行测试了,当点击按钮使调用我们封装的
public static void sendOkHttpRequest(String msg, okhttp3.Callback callback)方法,其中第一个参数填写自定义的消息内容,第二个参数是一个callback接口像下面一样就可以了。
注意:onFailure()和onResponse()方法中依然处于子线程中,不能在这两个方法中更新UI界面,即不能将返回的json信息直接显示在界面上,我们可以通过Log日志的形式将其打印出来。
/**
* 当点击发送按钮时
* 首先获取用户输入的信息,调用adapter.notifyDataSetChanged();方法将其显示到listview中
* 再调用我们封装的sendOkHttpRequest()方法从接口请求返回的数据
* 注意此时实现需要okhttp3.Callback接口中的onFailure()和onResponse()方法,分别表示请求失败和请求成功的情况
* onFailure()和onResponse()方法中依然处于子线程中,如果需要更新界面需要调用runOnUiThread()方法
* 或使用handler,我们这里选择使用handler
*/
btnSendRequest.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String message = etMsgContent.getText().toString().trim();
if(TextUtils.isEmpty(message)) {
return;
}
ChatMessage toMessage = new ChatMessage(message,new Date(), ChatMessage.Type.OUTCOMING);
data.add(toMessage);
// nowTime = System.currentTimeMillis();
// if(nowTime - lastTime > 5 * 1000) {
// tvToTime.setVisibility(View.VISIBLE);
// } else {
// tvToTime.setVisibility(View.GONE);
// }
// lastTime = nowTime;
adapter.notifyDataSetChanged();
lvChatMessage.setSelection(adapter.getCount()-1);
etMsgContent.setText("");
HttpUtils.sendOkHttpRequest(message,new okhttp3.Callback(){
@Override
public void onFailure(Call call, IOException e) {//请求过程中出现错误的回调
e.printStackTrace();
Toast.makeText(MainActivity.this,"请求过程中出错了",Toast.LENGTH_SHORT).show();
}
@Override
public void onResponse(Call call, final Response response) throws IOException {//请求成功的回调
final String strResponse = parJson(response.body().string());//从返回的Json数据中解析出有用的数据
ChatMessage fromMessage = new ChatMessage(strResponse,new Date(),ChatMessage.Type.INCOMING);
Message message2 = new Message();
message2.obj = fromMessage;
handler.sendMessage(message2);
}
});
}
});
发送“你好”,打印出来的日志大概是这样的。这和官方文档中展示的返回数据示例不太一样,因此我们需要根据具体的数据格式来解析json数据。
其实到这里我们的智能聊天机器人已经完成了,可以发送信息,可以返回数据。只是看起来有点low,发送的信息只能在程序中写死,返回的数据也是一大堆json数据,半天找不到重点。
至于如何做出精美的聊天界面就不是我们今天讨论的范围了。整个项目已经上传到github,点击这里进行下载。其实整个项目的实现在文章中三言两语是解释不清楚的,尤其是一些细节,很难顾及到,建议大家多研究github上面优秀的开源项目,在真正的项目中逐渐成长。
下面将整个项目的主要代码展示在下面
HttpUtils
package com.example.utils;
import android.util.Log;
import com.example.bean.Result;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.IOException;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class HttpUtils {
private static final String URL = "http://openapi.tuling123.com/openapi/api/v2";
private static final String API_KEY = "7bdfd1b20f084b8089eeaf289799c68d";
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
/**
* 使用OkHttp自身回调请求数据
* @param msg
* @param callback
* enqueue()方法中会自动开启线程
*/
public static void sendOkHttpRequest(String msg, okhttp3.Callback callback) {
OkHttpClient client = new OkHttpClient();
final String json = "{" +
"\"reqType\":0," +
" \"perception\": {" +
" \"inputText\": {" +
" \"text\": \"" + msg + "\"" +
" }," +
" \"inputImage\": {" +
" \"url\": \"\"" +
" }," +
" \"selfInfo\": {" +
" \"location\": {" +
" \"city\": \"天津\"," +
" \"province\": \"天津\",\n" +
" \"street\": \"天津理工大学\"\n" +
" }" +
" }" +
" }," +
" \"userInfo\": {" +
" \"apiKey\": \"" + API_KEY + "\"," +
" \"userId\": \"" + "572780350" + "\"" +
" }" +
"}";
RequestBody body = RequestBody.create(JSON,json);
Request request = new Request.Builder()
.url(URL)
.post(body)
.build();
//注意这里用的是enqueue()方法,此方法会在内部自动开启一个线程
client.newCall(request).enqueue(callback);
}
/**
* 自己构造回调方法请求数据,测试时使用
* @param msg
* @param listener
* 因为是网络请求,因此需要开启线程
*/
public static void doRequest(String msg, final HttpCallbackListener listener) {
final String json = "{" +
"\"reqType\":0," +
" \"perception\": {" +
" \"inputText\": {" +
" \"text\": \"" + msg + "\"" +
" }," +
" \"inputImage\": {" +
" \"url\": \"\"" +
" }," +
" \"selfInfo\": {" +
" \"location\": {" +
" \"city\": \"天津\"," +
" \"province\": \"天津\",\n" +
" \"street\": \"天津理工大学\"\n" +
" }" +
" }" +
" }," +
" \"userInfo\": {" +
" \"apiKey\": \"" + API_KEY + "\"," +
" \"userId\": \"" + "572780350" + "\"" +
" }" +
"}";
new Thread(new Runnable() {
@Override
public void run() {
String strResult = "";
Response responseData = null;
OkHttpClient client = new OkHttpClient();
RequestBody body = RequestBody.create(JSON,json);
Request request = new Request.Builder()
.url(URL)
.post(body)
.build();
try {
responseData = client.newCall(request).execute();
String response = responseData.body().string();
Log.d("xxx","请求成功" + responseData);
strResult = parJson(response);
if(listener != null) {
listener.finish(strResult);
}
} catch (IOException e) {
e.printStackTrace();
if(listener != null) {
listener.onError(e);
}
} finally {
responseData.body().close();
}
}
}).start();
}
private static String parJson(String responseData) {
Gson gson = new Gson();
String strResult = "";
Result result = gson.fromJson(responseData,new TypeToken<Result>(){}.getType());
strResult = result.getResults().get(0).getValues().getText();
Log.d("xxx","返回的结果为:" + strResult);
return strResult;
}
// {
// "emotion":
// {
// "robotEmotion":
// {
// "a":0,"d":0,"emotionId":0,"p":0
// },
// "userEmotion":
// {
// "a":0,"d":0,"emotionId":10300,"p":0
// }
// },
// "intent":
// {
// "actionName":"",
// "code":10004,
// "intentName":""
// },
// "results":
// [
// {
// "groupType":1,
// "resultType":"text",
// "values":
// {
// "text":"你陪我玩我就好啦"
// }
// }
// ]
// }
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
android:background="#F0F2F8"
tools:context="com.example.activity.MainActivity">
<android.support.v7.widget.Toolbar
android:id="@+id/tool_bar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@drawable/top_bar_bg"
app:title="小新"
android:layout_alignParentTop="true"
android:theme="@style/Base.ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/Theme.AppCompat.Light"/>
<ListView
android:id="@+id/list_chat_msg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/tool_bar"
android:layout_above="@+id/bottom_bar"
android:divider="@null"
android:dividerHeight="4dp"/>
<LinearLayout
android:id="@+id/bottom_bar"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_alignParentBottom="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="4dp">
<EditText
android:id="@+id/et_message_content"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="36dp"
android:layout_marginRight="4dp"
android:background="@drawable/edit_msg_bg"/>
<Button
android:id="@+id/btn_send_request"
android:layout_width="56dp"
android:layout_height="40dp"
android:minHeight="0dp"
android:maxLines="1"
android:background="@drawable/btn_sended"
android:textColor="#FFFFFF"
android:text="发送"/>
</LinearLayout>
</RelativeLayout>
MainActivity
package com.example.activity;
import android.media.MediaExtractor;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.example.adapter.ChatMessageAdapter;
import com.example.bean.ChatMessage;
import com.example.bean.Result;
import com.example.myrobot.R;
import com.example.utils.HttpCallbackListener;
import com.example.utils.HttpUtils;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import okhttp3.Call;
import okhttp3.Response;
public class MainActivity extends AppCompatActivity {
private Button btnSendRequest;
private EditText etMsgContent;
private ListView lvChatMessage;
private ChatMessageAdapter adapter;
private List<ChatMessage> data;
private TextView tvToTime;
private TextView tvFromTime;
private long lastTime;
private long nowTime;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
ChatMessage message = (ChatMessage) msg.obj;
data.add(message);
adapter.notifyDataSetChanged();
lvChatMessage.setSelection(adapter.getCount()-1);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initToolBar();
initData();
initView();
initEvent();
}
private void initData() {
data = new ArrayList<>();
data.add(new ChatMessage("很高兴为您服务,主人",new Date(), ChatMessage.Type.INCOMING));
lastTime = System.currentTimeMillis();
}
private void initEvent() {
/**
* 当点击发送按钮时
* 首先获取用户输入的信息,调用adapter.notifyDataSetChanged();方法将其显示到listview中
* 再调用我们封装的sendOkHttpRequest()方法从接口请求返回的数据
* 注意此时实现需要okhttp3.Callback接口中的onFailure()和onResponse()方法,分别表示请求失败和请求成功的情况
* onFailure()和onResponse()方法中依然处于子线程中,如果需要更新界面需要调用runOnUiThread()方法
* 或使用handler,我们这里选择使用handler
*/
btnSendRequest.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String message = etMsgContent.getText().toString().trim();
if(TextUtils.isEmpty(message)) {
return;
}
ChatMessage toMessage = new ChatMessage(message,new Date(), ChatMessage.Type.OUTCOMING);
data.add(toMessage);
// nowTime = System.currentTimeMillis();
// if(nowTime - lastTime > 5 * 1000) {
// tvToTime.setVisibility(View.VISIBLE);
// } else {
// tvToTime.setVisibility(View.GONE);
// }
// lastTime = nowTime;
adapter.notifyDataSetChanged();
lvChatMessage.setSelection(adapter.getCount()-1);
etMsgContent.setText("");
HttpUtils.sendOkHttpRequest(message,new okhttp3.Callback(){
@Override
public void onFailure(Call call, IOException e) {//请求过程中出现错误的回调
e.printStackTrace();
Toast.makeText(MainActivity.this,"请求过程中出错了",Toast.LENGTH_SHORT).show();
}
@Override
public void onResponse(Call call, final Response response) throws IOException {//请求成功的回调
final String strResponse = parJson(response.body().string());//从返回的Json数据中解析出有用的数据
ChatMessage fromMessage = new ChatMessage(strResponse,new Date(),ChatMessage.Type.INCOMING);
Message message2 = new Message();
message2.obj = fromMessage;
handler.sendMessage(message2);
}
});
}
});
etMsgContent.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
String content = etMsgContent.getText().toString().trim();
if(TextUtils.isEmpty(content)) {
btnSendRequest.setBackgroundResource(R.drawable.btn_sended);
} else {
btnSendRequest.setBackgroundResource(R.drawable.btn_sending);
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
}
private void initView() {
btnSendRequest = findViewById(R.id.btn_send_request);
etMsgContent = findViewById(R.id.et_message_content);
tvToTime = findViewById(R.id.tv_to_time);
tvFromTime = findViewById(R.id.tv_from_time);
lvChatMessage = findViewById(R.id.list_chat_msg);
adapter = new ChatMessageAdapter(this,data);
lvChatMessage.setAdapter(adapter);
}
private void initToolBar() {
Toolbar toolbar = findViewById(R.id.tool_bar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if(actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(false);
}
}
private String parJson(String responseData) {
Gson gson = new Gson();
String strResult = "";
Result result = gson.fromJson(responseData,new TypeToken<Result>(){}.getType());
strResult = result.getResults().get(0).getValues().getText();
Log.d("xxx","返回的结果为:" + strResult);
return strResult;
}
}
ChatMessageAdapter
package com.example.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import com.example.bean.ChatMessage;
import com.example.myrobot.R;
import java.text.SimpleDateFormat;
import java.util.List;
public class ChatMessageAdapter extends BaseAdapter {
private LayoutInflater mInflater;
private List<ChatMessage> mData;
public ChatMessageAdapter(Context context,List<ChatMessage> data) {
this.mInflater = LayoutInflater.from(context);
this.mData = data;
}
@Override
public int getCount() {
return mData.size();
}
@Override
public Object getItem(int position) {
return mData.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getItemViewType(int position) {
ChatMessage chatMessage = mData.get(position);
if(chatMessage.getType() == ChatMessage.Type.INCOMING) {
return 0;
}
return 1;
}
@Override
public int getViewTypeCount() {
return 2;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if(convertView == null) {
holder = new ViewHolder();
if(getItemViewType(position) == 0) {
convertView = mInflater.inflate(R.layout.item_from_layout,parent,false);
holder.tvMessageDate = convertView.findViewById(R.id.tv_from_time);
holder.tvMessageContent = convertView.findViewById(R.id.tv_from_msg_info);
} else {
convertView = mInflater.inflate(R.layout.item_to_layout,parent,false);
holder.tvMessageDate = convertView.findViewById(R.id.tv_to_time);
holder.tvMessageContent = convertView.findViewById(R.id.tv_to_msg_info);
}
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
ChatMessage message = mData.get(position);
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
holder.tvMessageDate.setText(format.format(message.getDate()));
holder.tvMessageContent.setText(message.getContent());
return convertView;
}
private class ViewHolder {
TextView tvMessageDate;
TextView tvMessageContent;
}
}
ChatMessage
package com.example.bean;
import java.util.Date;
public class ChatMessage {
private String content;
private Date date;
private Type type;
public enum Type{
INCOMING,OUTCOMING
}
public ChatMessage(String content, Date date, Type type) {
this.content = content;
this.date = date;
this.type = type;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public Type getType() {
return type;
}
public void setType(Type type) {
this.type = type;
}
}