教你如何在 Andorid 上使用OpenAI API 呼叫ChatGpt

weiweiyi發表於2023-03-26

前言

現在Chat GPT功能越來越強了,幾乎你想問實際問題它都能給你回答。
正好,小組結課的 Android專案 有一個解夢的功能。正好呼叫chatGpt的Api來實現。
下面就來簡單實現在Andorid專案中打造一個簡易的聊天機器人。

先貼個效果, 還沒進行美化:
OUT_20230325_160850.gif

1.建立一個Andorid專案

這裡就不在詳細地介紹如何建立一個Andorid專案了。谷歌上很多文章,比如官網示例:
https://developer.android.com/training/basics/firstapp/creati...

2.在 build.gradle 中新增依賴

這是用到的依賴

implementation 'com.google.android.material:material:1.7.0'
implementation 'com.android.volley:volley:1.2.0'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.9.3'
compileOnly 'org.projectlombok:lombok:1.18.4'
annotationProcessor 'org.projectlombok:lombok:1.18.4'
implementation 'org.apache.commons:commons-lang3:3.6'

3.在 AndroidManifest.xml 中新增聯網許可權

<uses-permission android:name="android.permission.INTERNET"/>

4. 註冊OpenAI api

來到 OpenAI api 註冊賬號,並生成 SECRET KEY
image.png

記得儲存好,之後會用到。

5.編寫xml介面檔案

在res/layout下新建activity_chat.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="@color/grey">

    <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_above="@id/idTILQuery"
            android:layout_alignParentTop="true"
            android:padding="5dp"
            android:layout_marginTop="5dp"
            android:layout_marginStart="5dp"
            android:layout_marginEnd="5dp"
            android:layout_marginBottom="5dp">

        <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">

            <!-- text view for displaying question-->
            <TextView
                    android:id="@+id/idTVQuestion"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_marginTop="30dp"
                    android:padding="4dp"
                    android:text="Question"
                    android:textColor="@color/purple_700"
                    android:textSize="17sp"/>

            <!-- text view for displaying response-->
            <TextView
                    android:id="@+id/idTVResponse"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_marginTop="5dp"
                    android:padding="4dp"
                    android:text="Response"
                    android:textColor="@color/purple_700"
                    android:textSize="15sp"/>
        </LinearLayout>

    </ScrollView>
    <!-- text field for asking question-->
    <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/idTILQuery"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_margin="5dp"
            app:endIconMode="custom"
            app:endIconDrawable="@drawable/send"
            android:hint="Enter your query"
            android:padding="5dp"
            android:textColorHint="@color/black"
            app:hintTextColor="@color/black">

        <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/idEdtQuery"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@color/grey"
                android:ems="10"
                android:imeOptions="actionSend"
                android:importantForAutofill="no"
                android:inputType="textEmailAddress"
                android:textColor="@color/black"
                android:textColorHint="@color/black"
                android:textSize="14sp" />
    </com.google.android.material.textfield.TextInputLayout>
</RelativeLayout>

一個簡單的頁面如下
image.png

5.編寫Activity檔案

繫結控制元件

首先,我們先建立了三個控制元件顯示內容:問題、響應、使用者輸入。
用 setContentView 繫結xml檔案,並使用 findViewById 給三個控制元件初始化

public class ChatActivity extends AppCompatActivity {
    TextView responseTV;
    TextView questionTV;
    TextInputLayout queryEdt;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        DynamicColors.applyToActivityIfAvailable(this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_chat);

        // initializing
        responseTV = findViewById(R.id.idTVResponse);
        questionTV = findViewById(R.id.idTVQuestion);
        queryEdt = findViewById(R.id.idTILQuery);
    }
}

監聽傳送按鈕

其次,我們需要監聽傳送按鈕,當使用者點選後傳送後,請求chatGpt。並顯示 Please wait..,讓使用者等待請求結果

@Override
    protected void onCreate(Bundle savedInstanceState) {
        DynamicColors.applyToActivityIfAvailable(this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_chat);
        // initializing variables on below line.
        responseTV = findViewById(R.id.idTVResponse);
        questionTV = findViewById(R.id.idTVQuestion);
        queryEdt = findViewById(R.id.idTILQuery);

        queryEdt.setEndIconOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                responseTV.setText("Please wait..");
                if (queryEdt.getEditText().getText().toString().length() > 0) {
                    getResponse(queryEdt.getEditText().getText().toString());
                } else {
                    Toast.makeText(ChatActivity.this, "Please enter your query..", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }

對 HttpURLConnection 設定代理

之後。我們應該對api進行請求了。由於眾所周知的原因,我們需要對 HttpURLConnection 設定代理:

假設當前的代理在192.168.2.197,7890埠
新建ProxiedHurlStack類, 填上代理伺服器的地址和埠。

public class ProxiedHurlStack extends HurlStack {

    @Override
    protected HttpURLConnection createConnection(URL url) throws IOException {
        // Start the connection by specifying a proxy server
        Proxy proxy = new Proxy(Type.HTTP,
                InetSocketAddress.createUnresolved("192.168.2.197", 7890));//the proxy server(Can be your laptop ip or company proxy)
        HttpURLConnection returnThis = (HttpURLConnection) url
                .openConnection(proxy);

        return returnThis;
    }
}

新建響應實體

根據OpenAI API Reference, 響應如下:

{
  "id": "chatcmpl-123",
  "object": "chat.completion",
  "created": 1677652288,
  "choices": [{
    "index": 0,
    "message": {
      "role": "assistant",
      "content": "\n\nHello there, how may I assist you today?",
    },
    "finish_reason": "stop"
  }],
  "usage": {
    "prompt_tokens": 9,
    "completion_tokens": 12,
    "total_tokens": 21
  }
}

所以,我們只需要獲取到 content 欄位, 就能我們和收到ChatGPT聊天的結果了。

對ChatGpt返回的響應,建立實體跟其對應

@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class ChatGptResponse {
    private String id;
    private String object;
    private Long created;
    private List<Choice> choices;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class Choice implements Serializable {
    private String index;
    private String finish_reason;
    private Message message;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class Message implements Serializable {
    private String role;
    private String content;
}

發起請求

之後,檢視OpenAI API Reference, 根據文件對api進行請求。這裡請求的模型是ChatGpt3.5。

記得在下面的程式碼中填寫你的SecretKey

 private void getResponse(String query) {
        try {
            questionTV.setText(query);
            RequestQueue requestQueue = Volley.newRequestQueue(this, new ProxiedHurlStack());
            String URL = "https://api.openai.com/v1/chat/completions";
            JSONObject jsonBody = new JSONObject();
            jsonBody.put("model", "gpt-3.5-turbo");
            JSONArray array = new JSONArray();
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("role", "user");
            jsonObject.put("content", query);
            array.put(jsonObject);
            jsonBody.put("messages", array);

            final String requestBody = jsonBody.toString();

            StringRequest stringRequest = new StringRequest(Request.Method.POST, URL, new Response.Listener<String>() {
                @Override
                public void onResponse(String response) {
                    Log.i("VOLLEY", response);
                }
            }, new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    Log.i("VOLLEY", String.valueOf(error));
                }
            }) {
                @Override
                public String getBodyContentType() {
                    return "application/json; charset=utf-8";
                }

                @Override
                public Map<String, String> getHeaders() throws AuthFailureError {
                    Map<String, String> params = new HashMap<String, String>();
                    params.put("Content-Type", "application/json");
                    params.put("Authorization", "Bearer yourSecretKey");
                    return params;
                }

                @Override
                public byte[] getBody() throws AuthFailureError {
                    try {
                        return requestBody == null ? null : requestBody.getBytes("utf-8");
                    } catch (UnsupportedEncodingException uee) {
                        VolleyLog.wtf("Unsupported Encoding while trying to get the bytes of %s using %s", requestBody, "utf-8");
                        return null;
                    }
                }

                @Override
                protected Response<String> parseNetworkResponse(NetworkResponse response) {
                    String responseString = new String(response.data, StandardCharsets.UTF_8);
                    // 建立ObjectMapper物件。
                    ObjectMapper mapper = new ObjectMapper();
                    // Json格式字串轉Java物件。
                    try {
                        ChatGptResponse javaEntity = mapper.readValue(responseString, ChatGptResponse.class);
                        String responseMsg = javaEntity.getChoices().get(0).getMessage().getContent();
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                responseTV.setText(responseMsg);
                            }
                        });
                    } catch (JsonProcessingException e) {
                        throw new RuntimeException(e);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }

                    return Response.success(responseString, HttpHeaderParser.parseCacheHeaders(response));
                }
            };

            stringRequest.setRetryPolicy(new RetryPolicy() {
                @Override
                public int getCurrentTimeout() {
                    return 50000;
                }

                @Override
                public int getCurrentRetryCount() {
                    return 50000;
                }

                @Override
                public void retry(VolleyError error) throws VolleyError {
                    Log.i("VOLLEY", String.valueOf(error));
                }
            });
            requestQueue.add(stringRequest);
        } catch (JSONException e) {
            e.printStackTrace();
        }

    }

我們在請求頭上填上我們的 SecretKey 作為認證資訊。

等待 ChatGpt 響應後,把響應的 byte[] 轉換為Java物件。

之後將資訊顯示在螢幕上。

目前我只取了響應的 content 資訊, 所以對話不是連續的,對接不了上文。

之後可以再參考 文件 將聊天做成連續的。

每個賬戶有 18美元 的免費額度,測試的時候 發了25個請求, 也才花了0.01美元。自己用著玩的話足夠使用了。
image.png

相關文章