Skip to content

Commit

Permalink
Facebook clean up for Inbox hand off
Browse files Browse the repository at this point in the history
  • Loading branch information
docwho2 committed Dec 11, 2023
1 parent 4114d29 commit 569773b
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 108 deletions.
49 changes: 44 additions & 5 deletions ChatGPT/src/main/java/cloud/cleo/squareup/ChatGPTLambda.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public class ChatGPTLambda implements RequestHandler<LexV2Event, LexV2Response>

public final static String TRANSFER_FUNCTION_NAME = "transfer_call";
public final static String HANGUP_FUNCTION_NAME = "hangup_call";
public final static String FACEBOOK_INBOX_FUNCTION_NAME = "facebook_inbox";

public final static String GENERAL_ERROR_MESG = "Sorry, I'm having a problem fulfilling your request. Please try again later.";

Expand All @@ -106,6 +107,8 @@ public class ChatGPTLambda implements RequestHandler<LexV2Event, LexV2Response>

// Create and init all the functions in the package
AbstractFunction.init();
// Hit static initializers in this as well so it's loaded and hot
new FaceBookOperations();
}

@Override
Expand Down Expand Up @@ -138,7 +141,18 @@ private LexV2Response processGPT(LexV2EventWrapper lexRequest) {
final var input = lexRequest.getInputTranscript();
final var inputMode = lexRequest.getInputMode();
final var attrs = lexRequest.getSessionAttributes();
// Will be phone if from SMS, Facebook the Page Scoped userID, Chime unique generated ID
final var session_id = lexRequest.getSessionId();

// Special Facebook Short Circut
if (attrs.containsKey(FACEBOOK_INBOX_FUNCTION_NAME)) {
log.debug("Facebook Short Circut, calling FB API to move thread to FB Inbox");
FaceBookOperations.transferToInbox(session_id);
// Clear out all sessions Attributes
attrs.clear();
// Send a close indicating we are done with this Lex Session
return buildTerminatingResponse(lexRequest, FACEBOOK_INBOX_FUNCTION_NAME, Map.of(), "Thread moved to Facebook Inbox.");
}

if (input == null || input.isBlank()) {
log.debug("Got blank input, so just silent or nothing");
Expand All @@ -160,8 +174,6 @@ private LexV2Response processGPT(LexV2EventWrapper lexRequest) {
attrs.put("blankCounter", "0");
}

// Will be phone if from SMS, Facebook the Page Scoped userID, Chime unique generated ID
final var session_id = lexRequest.getSessionId();
log.debug("Lex Session ID is " + session_id);

// Key to record in Dynamo which we key by date. So SMS/Facebook session won't span forever (by day)
Expand Down Expand Up @@ -273,6 +285,15 @@ private LexV2Response processGPT(LexV2EventWrapper lexRequest) {
} else {
log.debug("The following funtion calls were made " + functionCallsMade + " but none are terminating");
}

// Special Facebook handoff check
if (functionCallsMade.stream().anyMatch(f -> f.getName().equals(FACEBOOK_INBOX_FUNCTION_NAME))) {
// Session needs to move to Inbox, but we can't do it now because then our response won't make it to end user
// Push this into the Lex Session so on the next incoming message we can short circuit and call FB API
attrs.put(FACEBOOK_INBOX_FUNCTION_NAME, "true");
// Ignore what GPT said and send back message with Card asking how the bot did.
return buildResponse(lexRequest, "ChatBot has been removed from the conversation.", buildTransferCard());
}
}

// Since we have a general response, add message asking if there is anything else
Expand All @@ -287,7 +308,7 @@ private LexV2Response processGPT(LexV2EventWrapper lexRequest) {
// If this a new Session send back a Welcome card
return buildResponse(lexRequest, botResponse, buildWelcomeCard());
}

// Default response from GPT that is not a terminating action
return buildResponse(lexRequest, botResponse);
}
Expand Down Expand Up @@ -346,8 +367,8 @@ private LexV2Response buildResponse(LexV2EventWrapper lexRequest, String respons
.withContentType(PlainText.toString())
.withContent(response)
.build());
if (card != null) {

if (card != null) {
// Add a card if present
messages.add(LexV2Response.Message.builder()
.withContentType(ImageResponseCard.toString())
Expand Down Expand Up @@ -400,4 +421,22 @@ private ImageResponseCard buildWelcomeCard() {
).toArray(Button[]::new))
.build();
}

/**
* Transfer from Bot to Inbox Card
*
* @return
*/
private ImageResponseCard buildTransferCard() {
return com.amazonaws.services.lambda.runtime.events.LexV2Response.ImageResponseCard.builder()
.withTitle("Conversation moved to Inbox")
.withImageUrl("https://www.copperfoxgifts.com/logo.png")
.withSubtitle("Please tell us how our AI ChatBot did?")
.withButtons(List.of(
Button.builder().withText("Epic Fail").withValue("Chatbot was not Helpful.").build(),
Button.builder().withText("Needs Work").withValue("Chatbot needs some work.").build(),
Button.builder().withText("Great Job!").withValue("Chatbot did a great job!").build()
).toArray(Button[]::new))
.build();
}
}
126 changes: 126 additions & 0 deletions ChatGPT/src/main/java/cloud/cleo/squareup/FaceBookOperations.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package cloud.cleo.squareup;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import lombok.NonNull;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
* Perform various Facebook operations. Used when Channel is FB.
*
* @author sjensen
*/
public class FaceBookOperations {

// Initialize the Log4j logger.
private static final Logger log = LogManager.getLogger(FaceBookOperations.class);

private final static ObjectMapper mapper = new ObjectMapper();


/**
* Transfer control of Messenger Thread Session from Bot control to the Inbox. Used when end user needs to deal with
* a real person to resolve issue the Bot can't handle.
* https://developers.facebook.com/docs/messenger-platform/handover-protocol/conversation-control
*
* @param id
*/
public static void transferToInbox(String id) {
try {
HttpURLConnection connection = (HttpURLConnection) getFaceBookURL(System.getenv("FB_PAGE_ID"), "pass_thread_control").openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json; utf-8");
connection.setRequestProperty("Accept", "application/json");
connection.setDoOutput(true);

// Construct the payload
var json = mapper.createObjectNode();
// Special Target for Inbox
json.put("target_app_id", "263902037430900");
json.putObject("recipient").put("id", id);

log.debug("Post Payload for thread control " + json.toPrettyString());
mapper.writeValue(connection.getOutputStream(), json);

int responseCode = connection.getResponseCode();
log.debug("Facebook Call Response Code: " + responseCode);

final var result = mapper.readTree(connection.getInputStream());
log.debug("FB Pass Thread Control result is " + result.toPrettyString());

if (result.findValue("success") != null && result.findValue("success").asBoolean() == true) {
log.debug("Call Succeeded in passing thread control");
} else {
log.debug("Call FAILED to pass thread control");
}

} catch (Exception e) {
log.error("Facebook Pass Thread Control error", e);
}
}

/**
* Given a Facebook user Page Scoped ID get the users full name
*
* @param id
* @return
*/
public static String getFacebookName(String id) {
try {
HttpURLConnection connection = (HttpURLConnection) getFaceBookURL(id, null).openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Accept", "application/json");

int responseCode = connection.getResponseCode();
log.debug("Facebook Call Response Code: " + responseCode);

final var result = mapper.readTree(connection.getInputStream());
log.debug("FB Graph Query result is " + result.toPrettyString());

// Check for name first
if (result.findValue("name") != null) {
return result.findValue("name").asText();
}

// Usually returns first and last
if (result.findValue("first_name") != null && result.findValue("last_name") != null) {
return result.findValue("first_name").asText() + " " + result.findValue("last_name").asText();
}

} catch (Exception e) {
log.error("Facebook user name retrieval error", e);
}

return "Unknown";
}

/**
* Get the base URL for Facebook Graph Operations with page access token incorporated.
*
* @param id
* @param operation
* @return
* @throws MalformedURLException
*/
private static URL getFaceBookURL(@NonNull String id, String operation) throws MalformedURLException {
StringBuilder sb = new StringBuilder("https://graph.facebook.com/");

// Version of API we are calling
sb.append("v18.0/");

// ID for the entity we are using (Page ID, or Page scoped User ID)
sb.append(id);

// Optional operation
if (operation != null) {
sb.append('/').append(operation);
}

sb.append("?access_token=").append(System.getenv("FB_PAGE_ACCESS_TOKEN"));

return new URL(sb.toString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
import com.theokanning.openai.service.FunctionExecutor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
Expand All @@ -20,7 +18,6 @@
import java.util.regex.Pattern;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Expand Down Expand Up @@ -338,30 +335,4 @@ protected boolean isText() {
public boolean isTerminating() {
return false;
}

/**
* Get the base URL for Facebook Graph Operations with page access token incorporated.
* @param id
* @param operation
* @return
* @throws MalformedURLException
*/
protected URL getFaceBookURL(@NonNull String id, String operation) throws MalformedURLException {
StringBuilder sb = new StringBuilder("https://graph.facebook.com/");

// Version of API we are calling
sb.append("v18.0/");

// ID for the entity we are using (Page ID, or Page scoped User ID)
sb.append(id);

// Optional operation
if ( operation != null ) {
sb.append('/').append(operation);
}

sb.append("?access_token=").append(System.getenv("FB_PAGE_ACCESS_TOKEN"));

return new URL(sb.toString());
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package cloud.cleo.squareup.functions;

import static cloud.cleo.squareup.ChatGPTLambda.FACEBOOK_INBOX_FUNCTION_NAME;
import static cloud.cleo.squareup.enums.ChannelPlatform.FACEBOOK;
import static cloud.cleo.squareup.functions.AbstractFunction.log;
import static cloud.cleo.squareup.functions.AbstractFunction.mapper;
import java.net.HttpURLConnection;
import java.util.function.Function;

/**
Expand All @@ -17,12 +16,12 @@ public class FacebookInbox<Request> extends AbstractFunction {

@Override
public String getName() {
return "facebook_inbox";
return FACEBOOK_INBOX_FUNCTION_NAME;
}

@Override
public String getDescription() {
return "Should be called when the interaction requires a person to take over the conversation";
return "Should be called when the interaction requires a person to take over the conversation.";
}

@Override
Expand All @@ -32,43 +31,13 @@ public Class getRequestClass() {

/**
* https://developers.facebook.com/docs/messenger-platform/handover-protocol/conversation-control
*
* When this function is called we will control the response and perform the actual FB API call later.
*
* @return
*/
@Override
public Function<Request, Object> getExecutor() {
return (var r) -> {
try {
HttpURLConnection connection = (HttpURLConnection) getFaceBookURL(System.getenv("FB_PAGE_ID"), "pass_thread_control").openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json; utf-8");
connection.setRequestProperty("Accept", "application/json");
connection.setDoOutput(true);

// Construct the payload
var json = mapper.createObjectNode();
// Special Target for Inbox
json.put("target_app_id","263902037430900");
json.putObject("recipient").put("id", getSessionId());

log.debug("Post Payload for thread control " + json.toPrettyString());
mapper.writeValue(connection.getOutputStream(), json);

int responseCode = connection.getResponseCode();
log.debug("Facebook Call Response Code: " + responseCode);

final var result = mapper.readTree(connection.getInputStream());
log.debug("FB Pass Thread Control result is " + result.toPrettyString());

if (result.findValue("success") != null && result.findValue("success").asBoolean() == true) {
log.debug("Call Succeeded in passing thread control");
} else {
log.debug("Call FAILED to pass thread control");
}

} catch (Exception e) {
log.error("Facebook Pass Thread Control error", e);
}
return mapper.createObjectNode().put("message", "Conversation has been moved to the Inbox, a person will respond shortly.");
};
}
Expand Down
Loading

0 comments on commit 569773b

Please sign in to comment.