Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate with Conversational APIs #40

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
10 changes: 5 additions & 5 deletions config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"APP_ID": "4a557de8696f44bd99c49f6ba74295ad",
"APP_ID": "a569f8fb0309417780b793786b534a86",
"ENCRYPTION_ENABLED": false,
"ENABLE_GOOGLE_OAUTH": false,
"ENABLE_APPLE_OAUTH": false,
Expand All @@ -9,11 +9,11 @@
"MICROSOFT_CLIENT_ID": "",
"SLACK_CLIENT_ID": "",
"APPLE_CLIENT_ID": "",
"PROJECT_ID": "e0bf98db264ab3a02e8f",
"PROJECT_ID": "49c705c1c9efb71000d7",
"RECORDING_MODE": "MIX",
"APP_CERTIFICATE": "",
"CUSTOMER_ID": "",
"CUSTOMER_CERTIFICATE": "",
"APP_CERTIFICATE": "7b7a38bf722945d49cec839b1a78ef0d",
"CUSTOMER_ID": "2bedb79844ad40e5b8c794a5bf2fa66f",
"CUSTOMER_CERTIFICATE": "db1e754bb7af441b9725b281d3adfcd4",
"BUCKET_NAME": "",
"BUCKET_ACCESS_KEY": "",
"BUCKET_ACCESS_SECRET": "",
Expand Down
184 changes: 156 additions & 28 deletions customization/components/AgentControls/AgentContext.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,163 @@
import React, { createContext, useState } from 'react';
import {AIAgentState, AgentState} from "./const"
import React, {createContext, useState} from 'react';
import {AIAgentState, AgentState} from './const';
import {UidType} from 'customization-api';

export interface AgentContextInterface {
agentConnectionState:AIAgentState,
setAgentConnectionState: (agentState: AIAgentState) => void,
agentAuthToken: string|null,
setAgentAuthToken: (token: string | null) => void
export interface ChatItem {
id: string;
uid: UidType;
text: string;
isFinal: boolean;
time: number;
isSelf: boolean;
}

export interface AgentContextInterface {
agentConnectionState: AIAgentState;
setAgentConnectionState: (agentState: AIAgentState) => void;
agentAuthToken: string | null;
setAgentAuthToken: (token: string | null) => void;
isSubscribedForStreams: boolean;
setIsSubscribedForStreams: (state: boolean) => void;
agentUID: UidType | null;
setAgentUID: (uid: UidType | null) => void;
chatItems: ChatItem[];
addChatItem: (newItem: ChatItem) => void;
}

export const AgentContext = createContext<AgentContextInterface>({
agentConnectionState: AgentState.NOT_CONNECTED,
setAgentConnectionState: () => {},
agentAuthToken:null,
setAgentAuthToken: () => {}
})

export const AgentProvider: React.FC<{children: React.ReactNode}> = ({children}) => {
const [agentConnectionState, setAgentConnectionState] = useState<AIAgentState>(AgentState.NOT_CONNECTED);
const [agentAuthToken,setAgentAuthToken] = useState<string|null>(null)

const value = {
agentConnectionState,
setAgentConnectionState,
agentAuthToken,
setAgentAuthToken
agentConnectionState: AgentState.NOT_CONNECTED,
setAgentConnectionState: () => {},
agentAuthToken: null,
setAgentAuthToken: () => {},
isSubscribedForStreams: false,
setIsSubscribedForStreams: () => {},
agentUID: null,
setAgentUID: () => {},
chatItems: [],
addChatItem: () => {}, // Default no-op
});

/**
* Helper function to find the correct insertion index for a new item using binary search.
* Ensures that the array remains sorted by the `time` property after insertion.
*
* @param array The array to search within.
* @param time The `time` value of the new item to insert.
* @returns The index where the new item should be inserted.
*/
const findInsertionIndex = (array: ChatItem[], time: number): number => {
let low = 0;
let high = array.length;

// Perform binary search to find the insertion index
while (low < high) {
const mid = Math.floor((low + high) / 2);

// If the middle item's time is less than the new time, search the upper half
if (array[mid].time < time) {
low = mid + 1;
} else {
// Otherwise, search the lower half
high = mid;
}
}

return low; // The correct index for insertion
};

export const AgentProvider: React.FC<{children: React.ReactNode}> = ({
children,
}) => {
const [agentConnectionState, setAgentConnectionState] =
useState<AIAgentState>(AgentState.NOT_CONNECTED);
const [agentAuthToken, setAgentAuthToken] = useState<string | null>(null);
const [agentUID, setAgentUID] = useState<UidType | null>(null);
const [isSubscribedForStreams, setIsSubscribedForStreams] = useState(false);
const [chatItems, setChatItems] = useState<ChatItem[]>([]);

/**
* Adds a new chat item to the chat state while ensuring:
* - Outdated messages are discarded.
* - Non-finalized messages are updated if a newer message is received.
* - Finalized messages are added without duplication.
* - Chat items remain sorted by their `time` property.
*
* @param newItem The new chat item to add.
*/
const addChatItem = (newItem: ChatItem) => {
setChatItems(prevItems => {
// Find the index of the last finalized chat item for the same user
// Finalized messages are typically considered "complete" and should not be updated by non-final messages
const LastFinalIndex = prevItems.findLastIndex(
el => el.uid === newItem.uid && el.isFinal,
);

// Find the index of the last non-finalized chat item for the same user
// Non-finalized messages represent "in-progress" messages that can be updated or replaced
const LastNonFinalIndex = prevItems.findLastIndex(
el => el.uid === newItem.uid && !el.isFinal,
);

// Retrieve the actual items for the indices found above
const LastFinalItem =
LastFinalIndex !== -1 ? prevItems[LastFinalIndex] : null;
const LastNonFinalItem =
LastNonFinalIndex !== -1 ? prevItems[LastNonFinalIndex] : null;

// If the new message's timestamp is older than or equal to the last finalized message,
// it is considered outdated and discarded to prevent unnecessary overwrites.
if (LastFinalItem && newItem.time <= LastFinalItem.time) {
console.log(
'[AgentProvider] addChatItem - Discarded outdated message:',
newItem,
);
return prevItems; // Return the previous state without changes
}

// Create a new copy of the current chat items to maintain immutability
let updatedItems = [...prevItems];

// If there is a non-finalized message for the same user, replace it with the new message
if (LastNonFinalItem) {
console.log(
'[AgentProvider] addChatItem - Updating non-finalized message:',
newItem,
);
updatedItems[LastNonFinalIndex] = newItem; // Replace the non-finalized message
} else {
// If no non-finalized message exists, the new message is added to the array
console.log(
'[AgentProvider] addChatItem - Adding new message:',
newItem,
);

// Use binary search to find the correct insertion index for the new message
// This ensures the array remains sorted by the `time` property
const insertIndex = findInsertionIndex(updatedItems, newItem.time);

// Insert the new message at the correct position to maintain chronological order
updatedItems.splice(insertIndex, 0, newItem);
}

// Return the updated array, which will replace the previous state
return updatedItems;
});
};

const value = {
agentConnectionState,
setAgentConnectionState,
agentAuthToken,
setAgentAuthToken,
isSubscribedForStreams,
setIsSubscribedForStreams,
agentUID,
setAgentUID,
chatItems,
addChatItem, // Expose the function in the context
};

return (
<AgentContext.Provider value={value}>
{children}
</AgentContext.Provider>
)
}
return (
<AgentContext.Provider value={value}>{children}</AgentContext.Provider>
);
};
2 changes: 1 addition & 1 deletion customization/components/AgentControls/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const enum AgentState {

}

export const AI_AGENT_UID = 123;
export const AI_AGENT_UID = 123456;

// export const AGENT_PROXY_URL = "http://localhost:3000/api/proxy"
// export const AGENT_PROXY_URL = "https://conversational-ai-agent-git-testing-cors-agoraio.vercel.app/api/proxy"
Expand Down
59 changes: 31 additions & 28 deletions customization/components/AgentControls/index.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,40 @@
import React, { useContext, useEffect,useState } from 'react';
import { AI_AGENT_STATE, AIAgentState, AgentState, AI_AGENT_UID, AGENT_PROXY_URL} from "./const"
import { AI_AGENT_STATE, AIAgentState, AgentState, AGENT_PROXY_URL} from "./const"
import { TouchableOpacity, Text, ActivityIndicator } from "react-native";
import { AgentContext } from './AgentContext';
import Toast from "../../../react-native-toast-message/index";

import { isMobileUA, ThemeConfig, useContent, useEndCall } from "customization-api";
import { isMobileUA, ThemeConfig, UidType, useContent, useEndCall, useLocalUid } from "customization-api";
import { CallIcon, EndCall } from '../icons';
import { useHistory} from '../../../src/components/Router';
import StorageContext from '../../../src/components/StorageContext';

const connectToAIAgent = async (
agentAction: 'start_agent' | 'stop_agent',
agentAction: 'start' | 'stop',
channel_name: string,
clientId:string,agentAuthToken:string): Promise<string | void> => {
localUid:UidType,
agentAuthToken:string): Promise<{}> => {

// const apiUrl = '/api/proxy';
const apiUrl = AGENT_PROXY_URL;
const apiUrl = $config.BACKEND_ENDPOINT +'/v1/convoai';
const requestBody = {
// action: agentAction,
channel_name: channel_name,
uid: AI_AGENT_UID
};
uid: localUid // user uid // localUid or 0
}
console.log({requestBody})
const headers: HeadersInit = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${agentAuthToken}`,
'Authorization': `Bearer ${agentAuthToken}`

};

if (agentAction === 'stop_agent' && clientId) {
headers['X-Client-ID'] = clientId;
}


try {
const response = await fetch(`${apiUrl}/${agentAction}`, {
method: 'POST',
headers: headers,
body: JSON.stringify(requestBody),
mode: 'cors',
credentials: 'include'
});

if (!response.ok) {
Expand All @@ -47,25 +45,28 @@ const connectToAIAgent = async (

// console.log({data}, "X-Client-ID start stop")
console.log(
`AI agent ${agentAction === 'start_agent' ? 'connected' : 'disconnected'}`,
`AI agent ${agentAction === 'start' ? 'connected' : 'disconnected'}`,
data
);
if (agentAction === 'start_agent' && data.clientID) {
return data.clientID;
if (agentAction === 'start') {
return data;
}
} catch (error) {
console.error(`Failed to ${agentAction} AI agent connection:`, error);
throw error;
}
};

export const AgentControl: React.FC<{channel_name: string, style: object, clientId: string, setClientId: () => void}> = ({channel_name,style,clientId,setClientId}) => {
const {agentConnectionState, setAgentConnectionState,agentAuthToken, setAgentAuthToken} = useContext(AgentContext);
export const AgentControl: React.FC<{channel_name: string}> = ({channel_name}) => {
const {agentConnectionState, setAgentConnectionState,agentAuthToken, setAgentAuthToken,agentUID,setAgentUID} = useContext(AgentContext);
// console.log("X-Client-ID state", clientId)
// const { users } = useContext(UserContext)
const { activeUids:users } = useContent();
const { activeUids:users,defaultContent } = useContent();
const endcall = useEndCall();
const history = useHistory()
const {store} = React.useContext(StorageContext);
const localUid = useLocalUid();


// stop_agent API is successful, but agent has not yet left the RTC channel
const isAwaitingLeave = agentConnectionState === AgentState.AWAITING_LEAVE
Expand All @@ -83,11 +84,14 @@ export const AgentControl: React.FC<{channel_name: string, style: object, client
){
try{
setAgentConnectionState(AgentState.REQUEST_SENT);
const newClientId = await connectToAIAgent('start_agent', channel_name,'',agentAuthToken);
const data = await connectToAIAgent('start', channel_name,localUid,store.token);
// console.log("response X-Client-ID", newClientId, typeof newClientId)
if(typeof newClientId === 'string'){
setClientId(newClientId);
}
// @ts-ignore
const {agent_uid=null} = data;

//setClientId(agent_id);
setAgentUID(agent_uid)

setAgentConnectionState(AgentState.AWAITING_JOIN);
// toast({title: "Agent requested to join"})

Expand All @@ -114,7 +118,7 @@ export const AgentControl: React.FC<{channel_name: string, style: object, client
Toast.show({
leadingIconName: 'alert',
type: 'error',
text1: "Your session is expired. Please sing in to join call.",
text1: "Your session is expired. Please sign in to join call.",
text2: null,
visibilityTime: 5000,
primaryBtn: null,
Expand Down Expand Up @@ -154,7 +158,7 @@ export const AgentControl: React.FC<{channel_name: string, style: object, client
}
try{
setAgentConnectionState(AgentState.AGENT_DISCONNECT_REQUEST);
await connectToAIAgent('stop_agent', channel_name, clientId || undefined, agentAuthToken);
await connectToAIAgent('stop', channel_name,localUid, store.token);
setAgentConnectionState(AgentState.AWAITING_LEAVE);
// toast({ title: "Agent disconnecting..."})
Toast.show({
Expand Down Expand Up @@ -198,7 +202,7 @@ export const AgentControl: React.FC<{channel_name: string, style: object, client
useEffect(() => {
console.log("agent contrl", {users})
// welcome agent
const aiAgentUID = users.filter((item) => item === AI_AGENT_UID);
const aiAgentUID = users.filter((item) => item === agentUID);
if(aiAgentUID.length && agentConnectionState === AgentState.AWAITING_JOIN){
setAgentConnectionState(AgentState.AGENT_CONNECTED);
// toast({title: "Say Hi!!"})
Expand Down Expand Up @@ -268,7 +272,6 @@ export const AgentControl: React.FC<{channel_name: string, style: object, client
<Text style={{
fontFamily:ThemeConfig.FontFamily.sansPro,
...fontcolorStyle,
...style,
}}>{`${AI_AGENT_STATE[agentConnectionState]}` }</Text>
</TouchableOpacity>
)
Expand Down
1 change: 0 additions & 1 deletion customization/components/AudioVisualizer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import RtcEngine from "bridge/rtc/webNg";
import { AI_AGENT_UID } from "..";
import { isMobileUA, useContent, useLocalUid, useRtc } from "customization-api";
import React, { useEffect, useRef } from "react";
import { Text, View } from "react-native";
Expand Down
Loading