ARTC SDK-based integration solution

更新时间:
复制 MD 格式

This topic describes how to implement a Real-time Conversational AI solution using the Alibaba Real-Time Communication (ARTC) software development kit (SDK).

Introduction

This solution uses the ARTC SDK to build a Real-Time Communication (RTC) network and implements real-time interaction by calling Real-time Conversational AI API operations. This provides an efficient and flexible integration path. You can select API operations as needed to provide multiple artificial intelligence interaction experiences, such as intelligent conversation, sentiment analysis, matchmaking assistants, and digital human live streaming. You can also perform custom development and extend features for various application scenarios to improve the user experience.

Architecture

The following figure shows the system architecture for ARTC-based calls between users and AI agents.

image

Agent integration

Prerequisites

  1. You have created an agent. For more information, see Quick Start for Real-time Conversational AI.

  2. You need to integrate the Alibaba Real-Time Communication (ARTC) SDK. For instructions, see Getting Started with ApsaraVideo Real-time Communication.

Start and end a call

To start a call with an AI agent, follow these steps:

  1. A user sends a request from the application to the AppServer to start a call and obtain an RTC token.

  2. After the AppServer receives the request, it calls the StartAIAgentInstance operation to start the agent and generates an RTC token based on the rules in Token-based authentication.

  3. The AppServer returns the call result and the RTC token to the user.

  4. The user uses the ARTC SDK to join the corresponding RTC channel with the RTC token and starts the call with the agent.

To end a call with an AI agent, follow these steps:

  1. A user sends a request to the AppServer to end the call. The AppServer calls the StopAIAgentInstance operation to stop the AI agent.

  2. The user uses the ARTC SDK to leave the channel and end the session.

image

Development Reference

You can implement features by referring to the AICallKit integration solution. For client sample code, see Android source code and iOS source code. For server-side sample code, see Server source code.

Feature implementation

Note

The following features are implemented in the audio and video call agent integration solution. You can also use standard ARTC SDK API operations.

Agent status

Prerequisites

Parse RTC custom messages

The agent has three status codes: Listening, Thinking, and Speaking. You can parse the message body in the RTC custom message channel to obtain the agent's status code. The built-in message fields are as follows:

Field name

Description

type

The message type.

senderId

The user ID of the sender.

receiverId

The user ID of the recipient. If the message is a broadcast, this field is an empty string.

data

The message content. This field can be empty if there is no content.

state

The agent status code.

The message body is as follows:

 {
  "type": 1001,
  "data": {
    "state": 1,             // 1: Listening, 2: Thinking, 3: Speaking
  }
  "senderId": "robot_1",    // Sender ID
  "receiverId": ""          // The recipient ID does not need to be specified and is usually empty.
}
iOS

The following sample code shows how to obtain the agent status from the RTC custom message channel:

public func onDataChannelMessage(_ uid: String, controlMsg: AliRtcDataChannelMsg) {
    if controlMsg.type != .custom {
        return
    }

    let dataDict = (try? JSONSerialization.jsonObject(with: controlMsg.data, options: .allowFragments)) as? [String : Any]
    guard let dataDict = dataDict else {
        return
    }
    debugPrint("onDataChannelMessage:\(dataDict)")
    if dataDict["type"] as? Int32 == 1001 {
        let senderId = dataDict["senderId"] as? String
        let receiverId = dataDict["receiverId"] as? String
        let data = dataDict["data"] as? [String: Any]
        if let state = data?["state"] as? Int32 {
            DispatchQueue.main.async {
                // Update your UI state.
                debugPrint("Received Robot State Changed: \(state)")
            }
        }
    }
}
Android

The following sample code shows how to obtain the agent status from the RTC custom message channel:

aliRtcEngine.setRtcEngineNotify( new AliRtcEngineNotify() {
    @Override
    public void onDataChannelMessage(String uid, AliRtcEngine.AliRtcDataChannelMsg msg) {
        super.onDataChannelMessage(uid, msg);

        if (msg.type == AliEngineDataMsgCustom) {
            try {
                String dataStr = new String(msg.data);
                JSONObject jsonObject = new JSONObject(dataStr);
                int msgType = jsonObject.optInt("type");
                String senderId = jsonObject.optString("senderId");
                String receiverId = jsonObject.optString("receiverId");
                JSONObject dataJson = jsonObject.optJSONObject("data");
                if (null != dataJson) {

                    if (msgType == 1001) {
                        int robotState = dataJson.optInt("state");
                        ARTCAICallRobotState artcaiCallRobotState = null;
                        if (robotState == IMsgTypeDef.ROBOT_STATE.ROBOT_STATE_LISTENING) {
                            // TODO: Handle the agent's listening state.
                        } else if (robotState == IMsgTypeDef.ROBOT_STATE.ROBOT_STATE_THINKING) {
                            // TODO: Handle the agent's thinking state.
                        } else if (robotState == IMsgTypeDef.ROBOT_STATE.ROBOT_STATE_SPEAKING) {
                            // TODO: Handle the agent's speaking state.
                        }
                    }
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
});

Real-time captions

Prerequisites

Parse RTC custom messages

You must parse the message body in the RTC custom message channel to obtain the real-time caption content. The built-in message fields are as follows:

Field

Description

type

The message type:

  • type=1002: Real-time captions for the AI agent.

  • type=1003: Real-time captions for the end user.

  • type=1011: Custom message sent to the agent.

senderId

The ID of the agent that sends the message.

receiverId

The user ID of the recipient. This is an empty string.

data

The message content. This field can be empty if there is no content.

text

  • If type=1002, this is the specific text generated by the AI agent.

  • If type=1003, this is the specific text recognized from the end user's speech.

end

  • If type=1002, captions for long agent responses are split and sent in multiple parts. If end=true, this is the last part of the response.

  • If type=1003:

    • end=true: The text is the final recognition result for the sentence.

    • end=false: The text is an intermediate result during continuous recognition.

sentenceId

  • If type=1002, this indicates that the message is the large language model (LLM) content responding to the speech input with the corresponding sentenceId.

  • If type=1003, this is the ID of the sentence to which the current text belongs.

The message body for real-time captions from the AI agent is as follows:

{
  "type": 1002,
  "senderId": "robot_1",    // ID of the agent that sends the message
  "receiverId": "",   			// The recipient ID does not need to be specified and is usually empty.
  "data": {
    "text": "This is the text content generated by the AI agent.",  // Specific text generated by the AI agent
    "end": false,                      // Indicates whether the returned text is the last sentence of this response.
    "sentenceId": 1            		 // Indicates that this is the LLM content responding to the speech input with the corresponding sentenceId.
  }
}

The message body for real-time captions from the end user is as follows:

{
  "type": 1003,
  "senderId": "robot_1",    // ID of the agent that sends the message
  "receiverId": "",   			// The recipient ID does not need to be specified and is usually empty.
  "data": {
    "text": "This is the text content currently recognized from the end user's speech.",  // Specific text recognized from the end user's speech
    "end": false,                       // Indicates whether the current text is the final result for this sentence.
    "sentenceId": 1                     // The ID of the sentence to which the current text belongs.
  }
}
iOS

The following sample code shows how to retrieve real-time captions from the RTC custom message channel:

private var lastRobotSentenceId: Int? = nil
private var robotSpeakingText: String? = nil
public func onDataChannelMessage(_ uid: String, controlMsg: AliRtcDataChannelMsg) {    
    if controlMsg.type != .custom {
        return
    }

    let dataDict = (try? JSONSerialization.jsonObject(with: controlMsg.data, options: .allowFragments)) as? [String : Any]
    guard let dataDict = dataDict else {
        return
    }
    debugPrint("onDataChannelMessage:\(dataDict)")
    if dataDict["type"] as? Int32 == 1002 {
        let senderId = dataDict["senderId"] as? String
        let receiverId = dataDict["receiverId"] as? String
        let data = dataDict["data"] as? [String: Any]
        if let data = data {
            let text = data["text"] as? String
            let end = data["end"] as? Bool
            let sentenceId = data["sentenceId"] as? Int
            if sentenceId == lastRobotSentenceId {
                self.robotSpeakingText?.append(text ?? "") 
            }
            else {
                self.lastRobotSentenceId = sentenceId
                self.robotSpeakingText = text ?? ""
            }
            if end == true {
                DispatchQueue.main.async {
                    debugPrint("Received Robot Speaking Text: \(self.robotSpeakingText!)")
                    // Update your UI state.

                }
            }
        }
        if let text = data?["text"] as? String {
            
            DispatchQueue.main.async {
                // Update your UI state.
                debugPrint("Received Robot Speaking Text: \(text)")
            }
        }
    }
    else if dataDict["type"] as? Int32 == 1003 {
        let senderId = dataDict["senderId"] as? String
        let receiverId = dataDict["receiverId"] as? String
        let data = dataDict["data"] as? [String: Any]
        if let text = data?["text"] as? String {
            if data?["end"] as? Bool == true {
                DispatchQueue.main.async {
                    debugPrint("Received ASR Text: \(text)")
                    // Update your UI state.
                }
            }
        }
    }
}
Android

The following sample code shows how to retrieve real-time captions from the RTC custom message channel:

private int mSentenceId = -1;
private String mRobotSpeakingText = "";

aliRtcEngine.setRtcEngineNotify( new AliRtcEngineNotify() {
    @Override
    public void onDataChannelMessage(String uid, AliRtcEngine.AliRtcDataChannelMsg msg) {
        super.onDataChannelMessage(uid, msg);
        if (msg.type == AliEngineDataMsgCustom) {
            try {
                String dataStr = new String(msg.data);
                JSONObject jsonObject = new JSONObject(dataStr);
                int msgType = jsonObject.optInt("type");
                String senderId = jsonObject.optString("senderId");
                String receiverId = jsonObject.optString("receiverId");
                JSONObject dataJson = jsonObject.optJSONObject("data");
                if (null != dataJson) {
                    if (msgType == 1002) {
                        // Agent's speech
                        String text = dataJson.optString("text");
                        // Indicates whether the current text is the final result for this sentence.
                        boolean end = dataJson.optBoolean("end");
                        // Indicates that this is the LLM content responding to the speech input with the corresponding sentenceId.
                        int sentenceId = dataJson.optInt("sentenceId");
                        if (sentenceId == mSentenceId) {
                            mRobotSpeakingText += text;
                        }
                        else {
                            mRobotSpeakingText = text;
                            mSentenceId = sentenceId;
                        }
                        if (end) {
                            System.out.println(mRobotSpeakingText);
                            // TODO: Display agent captions.
                        }
                        
                    } else if (msgType == 1003) {
                        // Specific text recognized by ASR.
                        String text = dataJson.optString("text");
                        // Indicates whether the current text is the final result for this sentence.
                        boolean end = dataJson.optBoolean("end");
                        // The ID of the sentence to which the current text belongs.
                        int sentenceId = dataJson.optInt("sentenceId");
                        if (end) {
                            System.out.println(text);
                            // TODO: Display ASR captions.
                        }
                    }
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
});

Interrupt the agent's speech

Send an interruption message

Interruption messages are sent through the RTC custom message channel. You must first activate an ApsaraVideo Real-time Communication application and enable the RTC custom message channel. For more information, see Getting Started with ApsaraVideo Real-time Communication and Send and receive custom messages.

An interruption message must contain the following fields:

Field

Description

type

The message type.

senderId

The user ID of the sender.

receiverId

The ID of the agent that receives the message.

The interruption message is as follows:

{
  "type": 1101,
  "senderId": "user_1",    // Sender ID
  "receiverId": "robot_1"  // Agent ID
}
iOS

The following sample code shows how the client ARTC SDK sends an instruction to interrupt the agent's speech through the custom message channel:

var sendDict: [String: Any] = [
    "type": 1101,
]
sendDict.updateValue(myUid, forKey: "senderId")
sendDict.updateValue(robotUid, forKey: "receiverId")

if let sendData = sendDict.aicall_jsonString.data(using: .utf8) {
    let rtcMsg = AliRtcDataChannelMsg()
    rtcMsg.type = .custom
    rtcMsg.data = sendData
    self.rtcEngine.sendDataChannelMessage(rtcMsg)
}
Android

The following sample code shows how the client ARTC SDK sends an instruction to interrupt the agent's speech through the custom message channel:

int msgType = 1101;
// The userId of the user after joining the channel.
String senderId = "myRtcUserId";
// The userId of the agent after joining the channel.
String receiverId = "robotRtcUserId";

if (null != mAliRtcEngine) {
    try {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("type", msgType);
        jsonObject.put("senderId", senderId);
        jsonObject.put("receiverId", receiverId);
        AliRtcEngine.AliRtcDataChannelMsg rtcDataChannelMsg = new AliRtcEngine.AliRtcDataChannelMsg();
        rtcDataChannelMsg.type = AliEngineDataMsgCustom;
        rtcDataChannelMsg.data = jsonObject.toString().getBytes(StandardCharsets.UTF_8);
        mAliRtcEngine.sendDataChannelMsg(rtcDataChannelMsg);
    } catch (JSONException ex) {
        ex.printStackTrace();
    }
}

Walkie-talkie mode

Prerequisites

Interaction flow

image

Message protocol

Enable or disable walkie-talkie mode

Message type: 1105

The message body is as follows:

{
  "type": 1105,
  "senderId": "user_1",     // Sender ID
  "receiverId": "robot_1",  // Agent ID
  "data": {
    "enable": true          // true: enables walkie-talkie mode. false: disables walkie-talkie mode.
  }
}
Transmit walkie-talkie mode status

Message type: 1007

The message body is as follows:

{
  "type": 1007,
  "seqId": 5,
  "senderId": "robot_1",    // AI agent UID
  "receiverId": "",   			// The recipient ID does not need to be specified and is usually empty.
  "data": {
    "enable": true       // true: the agent has entered walkie-talkie mode. false: the agent has exited walkie-talkie mode.
  }
}
Note

The agent service sends this message to notify you of the result, regardless of whether walkie-talkie mode was enabled or disabled through an OpenAPI call or a DataChannel message.

Press to start speaking

Message type: 1106

The message body is as follows:

{
  "type": 1106,
  "senderId": "user_1",    // Sender ID
  "receiverId": "robot_1"  // Agent ID
}
Release to send speech

Message type: 1107

The message body is as follows:

{
  "type": 1107,
  "senderId": "user_1",    // Sender ID
  "receiverId": "robot_1"  // Agent ID
}
Release to cancel speaking

Message type: 1108

The message body is as follows:

{
  "type": 1108,
  "senderId": "user_1",    // Sender ID
  "receiverId": "robot_1"  // Agent ID
}

Code examples

iOS
// Enable or disable walkie-talkie mode.
public func enablePushToTalk(enable: Bool) {
    var sendDict: [String: Any] = [
        "type": 1105,
    ]
    sendDict.updateValue(myUid, forKey: "senderId")
    sendDict.updateValue(robotUid, forKey: "receiverId")
    sendDict.updateValue(["enable": enable], forKey: "data")
    
    if let sendData = sendDict.aicall_jsonString.data(using: .utf8) {
        let rtcMsg = AliRtcDataChannelMsg()
        rtcMsg.type = .custom
        rtcMsg.data = sendData
        self.rtcEngine.sendDataChannelMessage(rtcMsg)

        self.rtcEngine.muteLocalMic(enable, mode: .allAudioMode)
    }
}

// Press and hold to speak. Make sure walkie-talkie mode is enabled.
public func startPushToTalk() {
    var sendDict: [String: Any] = [
        "type": 1106,
    ]
    sendDict.updateValue(myUid, forKey: "senderId")
    sendDict.updateValue(robotUid, forKey: "receiverId")
    
    if let sendData = sendDict.aicall_jsonString.data(using: .utf8) {
        let rtcMsg = AliRtcDataChannelMsg()
        rtcMsg.type = .custom
        rtcMsg.data = sendData
        self.rtcEngine.sendDataChannelMessage(rtcMsg)
        // Unmute the microphone.
        self.rtcEngine.muteLocalMic(false, mode: .allAudioMode)
    }
}

// Finish speaking. Make sure walkie-talkie mode is enabled.
public func finishPushToTalk() {
    var sendDict: [String: Any] = [
        "type": 1107,
    ]
    sendDict.updateValue(myUid, forKey: "senderId")
    sendDict.updateValue(robotUid, forKey: "receiverId")
    
    if let sendData = sendDict.aicall_jsonString.data(using: .utf8) {
        let rtcMsg = AliRtcDataChannelMsg()
        rtcMsg.type = .custom
        rtcMsg.data = sendData
        self.rtcEngine.sendDataChannelMessage(rtcMsg)
        // Mute the microphone.
        self.rtcEngine.muteLocalMic(true, mode: .allAudioMode)
    }
}

// Cancel this transmission. Make sure walkie-talkie mode is enabled.
public func cancelPushToTalk() {
    var sendDict: [String: Any] = [
        "type": 1108,
    ]
    sendDict.updateValue(myUid, forKey: "senderId")
    sendDict.updateValue(robotUid, forKey: "receiverId")
    
    if let sendData = sendDict.aicall_jsonString.data(using: .utf8) {
        let rtcMsg = AliRtcDataChannelMsg()
        rtcMsg.type = .custom
        rtcMsg.data = sendData
        self.rtcEngine.sendDataChannelMessage(rtcMsg)
        // Mute the microphone.
        self.rtcEngine.muteLocalMic(true, mode: .allAudioMode)
    }
}

// Process received DataChannel messages.
public func onDataChannelMessage(_ uid: String, controlMsg: AliRtcDataChannelMsg) {    
    if controlMsg.type != .custom {
        return
    }

    let dataDict = (try? JSONSerialization.jsonObject(with: controlMsg.data, options: .allowFragments)) as? [String : Any]
    guard let dataDict = dataDict else {
        return
    }
    debugPrint("onDataChannelMessage:\(dataDict)")
    if dataDict["type"] as? Int32 == 1007 {
        let data = dataDict["data"] as? [String: Any]
        if let data = data {
            if let enable = data["enable"] as? Bool {
                // The server has enabled or disabled walkie-talkie mode. Refer to the flow.
                DispatchQueue.main.async {
                    self.enablePushToTalk = enable
                    self.rtcEngine.muteLocalMic(enable, mode: .allAudioMode)
                }
            }
            
        }
    }
    // Process other messages.
    ...
}
Android
public void sendCustomMessage(int msgType, JSONObject data) {
    if (null != mAliRtcEngine) {
        try {
            String senderId; // Your user ID
            String receiverId; // Agent user ID

            JSONObject jsonObject = new JSONObject();
            jsonObject.put("type", msgType);
            jsonObject.put("senderId", senderId);
            jsonObject.put("receiverId", receiverId);
            if (null != data) {
                jsonObject.put("data", data);
            }
            AliRtcEngine.AliRtcDataChannelMsg rtcDataChannelMsg = new AliRtcEngine.AliRtcDataChannelMsg();
            rtcDataChannelMsg.type = AliEngineDataMsgCustom;
            rtcDataChannelMsg.data = jsonObject.toString().getBytes(StandardCharsets.UTF_8);
            mAliRtcEngine.sendDataChannelMsg(rtcDataChannelMsg);
        } catch (JSONException ex) {
            ex.printStackTrace();
        }
    }
}

// Enable or disable walkie-talkie mode.
public boolean enablePushToTalk(boolean enable) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("enable", enable);
// Send a message to the agent.
sendCustomMessage(1105, jsonObject);
// Mute the microphone.
mAliRtcEngine.muteLocalMic(enable, AliRtcEngine.AliRtcMuteLocalAudioMode.AliRtcMuteAllAudioMode);
}

// Press and hold to speak. Make sure walkie-talkie mode is enabled.
public func startPushToTalk() {
    // Send a message to the agent.
    sendCustomMessage(1106, null);
    // Unmute the microphone.
    mAliRtcEngine.muteLocalMic(false, AliRtcEngine.AliRtcMuteLocalAudioMode.AliRtcMuteAllAudioMode);
}

// Finish speaking. Make sure walkie-talkie mode is enabled.
public func finishPushToTalk() {
    // Send a message to the agent.
    sendCustomMessage(1107, null);
    // Mute the microphone.
    mAliRtcEngine.muteLocalMic(true, AliRtcEngine.AliRtcMuteLocalAudioMode.AliRtcMuteAllAudioMode);
}

// Cancel this transmission. Make sure walkie-talkie mode is enabled.
public func cancelPushToTalk() {
    // Send a message to the agent.
    sendCustomMessage(1108, null);
    // Mute the microphone.
    mAliRtcEngine.muteLocalMic(true, AliRtcEngine.AliRtcMuteLocalAudioMode.AliRtcMuteAllAudioMode);
}

// Process received DataChannel messages.
private AliRtcEngineNotify mRtcEngineRemoteNotify = new AliRtcEngineNotify() {

    @Override
    public void onDataChannelMessage(String uid, AliRtcEngine.AliRtcDataChannelMsg msg) {
        super.onDataChannelMessage(uid, msg);
        if (msg.type == AliEngineDataMsgCustom) {
            String dataStr = new String(msg.data);
            JSONObject jsonObject = new JSONObject(dataStr);
            JSONObject dataJson = jsonObject.optJSONObject("data");
            int msgType = jsonObject.optInt("type");
            if (msgType == 1007) {
                boolean enable = dataJson.optBoolean("enable");
                // The agent notifies the walkie-talkie mode status. Handle business logic...
            }
        }
    }
}
Web
// Enable or disable walkie-talkie mode.
public enablePushToTalk(enable: boolean) { }

// Press and hold to speak. Make sure walkie-talkie mode is enabled.
public startPushToTalk(): boolean { }

// Finish speaking. Make sure walkie-talkie mode is enabled.
public finishPushToTalk(): boolean { }

// Cancel this transmission. Make sure walkie-talkie mode is enabled.
public cancelPushToTalk(): boolean { }

controller.on('AICallPushToTalkChanged', (enable: boolean) => {
    // The current walkie-talkie mode has changed to {enable}.
})

Client receives custom messages from the server

During a call, when the server pushes custom business messages to the client using the OpenAPI SendAIAgentDataChannelMessage operation, the client must receive and process these messages.

Prerequisites

Parse RTC custom messages

Field

Description

type

The message type. The value is 1011.

seqId

The message ID. You do not usually need to process this.

senderId

The AI agent UID.

receiverId

The user ID of the recipient. You do not usually need to process this.

data

Encapsulates the custom message.

data.message

The custom message content in a JSON string.

The message body is as follows:

{
  "type": 1011,
  "seqId": 1,
  "data": {
    "message": "{}"         // Message content in a JSON string
  }
  "senderId": "robot_1",    // AI agent UID
  "receiverId": ""          // The recipient ID does not need to be specified and is usually empty.
}

Client receives notifications when the LLM completes its response

If the ARTCAICallAgentLlmConfig.llmCompleteReply parameter is enabled when you start the call, the agent sends a message to the client after the LLM node finishes processing the current question. The client can listen for this message and process the returned text, which includes the LLM's response for the current turn.

Prerequisites

Parse RTC custom messages

Field name

Description

type

The message type. The value is 1017.

seqId

The message ID. You do not usually need to process this.

senderId

The AI agent UID.

receiverId

The user ID of the recipient. You do not usually need to process this.

data

Encapsulates the custom message.

data.text

The specific text generated by the LLM.

data.sentenceId

Indicates that this is the LLM content responding to the speech input with the corresponding sentenceId.

The message body is as follows:

{
  "type": 1017,
  "seqId": 4,
  "senderId": "robot_1",    // AI agent UID
  "receiverId": "",   			// The recipient ID does not need to be specified and is usually empty.
  "data": {
    "text": "This is the text content generated by the LLM.",  // Specific text generated by the LLM
    "sentenceId": 1            		 // Indicates that this is the LLM content responding to the speech input with the corresponding sentenceId.
  }
}