Skip to content

Commit

Permalink
Channel Platform work and clean ups
Browse files Browse the repository at this point in the history
  • Loading branch information
docwho2 committed Dec 8, 2023
1 parent 98844c3 commit 7a35543
Show file tree
Hide file tree
Showing 12 changed files with 103 additions and 52 deletions.
52 changes: 27 additions & 25 deletions ChatGPT/src/main/java/cloud/cleo/squareup/ChatGPTLambda.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
*/
package cloud.cleo.squareup;

import static cloud.cleo.squareup.LexInputMode.TEXT;
import cloud.cleo.squareup.enums.LexInputMode;
import static cloud.cleo.squareup.enums.LexInputMode.TEXT;
import cloud.cleo.squareup.functions.AbstractFunction;
import cloud.cleo.squareup.json.DurationDeserializer;
import cloud.cleo.squareup.json.DurationSerializer;
Expand Down Expand Up @@ -39,6 +40,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletionException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import software.amazon.awssdk.enhanced.dynamodb.DynamoDbAsyncTable;
Expand Down Expand Up @@ -76,6 +78,8 @@ 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 GENERAL_ERROR_MESG = "Sorry, I'm having a problem fulfilling your request. Please try again later.";

// Eveverything here will be done at SnapStart init
static {
Expand Down Expand Up @@ -118,10 +122,13 @@ public LexV2Response handleRequest(LexV2Event lexRequest, Context cntxt) {
processGPT(lexRequest);
};

} catch (CompletionException e) {
log.error("Unhandled Future Exception", e.getCause());
return buildResponse(lexRequest, GENERAL_ERROR_MESG);
} catch (Exception e) {
log.error("Unhandled Exception", e);
// Unhandled Exception
return buildResponse(lexRequest, "Sorry, I'm having a problem fulfilling your request. Chat GPT might be down, Please try again later.");
return buildResponse(lexRequest, GENERAL_ERROR_MESG);
}
}

Expand Down Expand Up @@ -161,9 +168,7 @@ private LexV2Response processGPT(LexV2Event lexRequest) {
final var key = Key.builder().partitionValue(session_id).sortValue(LocalDate.now(ZoneId.of("America/Chicago")).toString()).build();

// load session state if it exists
log.debug("Start Retreiving Session State");
var session = sessionState.getItem(key).join();
log.debug("End Retreiving Session State");

if (session == null) {
session = new ChatGPTSessionState(session_id, inputMode);
Expand All @@ -176,7 +181,7 @@ private LexV2Response processGPT(LexV2Event lexRequest) {

String botResponse;
// Store all the calls made
List<ChatFunctionCall> callsMade = new LinkedList<>();
List<ChatFunctionCall> functionCallsMade = new LinkedList<>();
try {
FunctionExecutor functionExecutor = AbstractFunction.getFunctionExecuter(lexRequest);
functionExecutor.setObjectMapper(mapper);
Expand Down Expand Up @@ -224,7 +229,7 @@ private LexV2Response processGPT(LexV2Event lexRequest) {
log.debug("Executed " + functionCall.getName() + ".");
session.addMessage(message.get());
// Track each call made
callsMade.add(functionCall);
functionCallsMade.add(functionCall);
continue;
} else {
log.debug("Something went wrong with the execution of " + functionCall.getName() + "...");
Expand All @@ -240,10 +245,8 @@ private LexV2Response processGPT(LexV2Event lexRequest) {
}

// Save the session to dynamo
log.debug("Start Saving Session State");
session.incrementCounter();
sessionState.putItem(session).join();
log.debug("End Saving Session State");
} catch (RuntimeException rte) {
if (rte.getCause() != null && rte.getCause() instanceof SocketTimeoutException) {
log.error("Response timed out", rte);
Expand All @@ -255,22 +258,21 @@ private LexV2Response processGPT(LexV2Event lexRequest) {

log.debug("botResponse is [" + botResponse + "]");

if ( ! callsMade.isEmpty() ) {
if (!functionCallsMade.isEmpty()) {
// Did a terminating function get executed
final var termCalled = callsMade.stream()
final var termCalled = functionCallsMade.stream()
.map(f -> AbstractFunction.getFunctionByName(f.getName()))
.filter(f -> f.isTerminating() )
.filter(f -> f.isTerminating())
.findAny()
.orElse(null);
if ( termCalled != null ) {
if (termCalled != null) {
log.debug("A termianting function was called = [" + termCalled.getName() + "]");
final ChatFunctionCall gptFunCall = callsMade.stream().filter(f -> f.getName().equals(termCalled.getName())).findAny().get();
final ChatFunctionCall gptFunCall = functionCallsMade.stream().filter(f -> f.getName().equals(termCalled.getName())).findAny().get();
final var args = mapper.convertValue(gptFunCall.getArguments(), Map.class);
return buildTerminatingResponse(lexRequest, gptFunCall.getName(), args, botResponse);
} else {
log.debug("The following funtion calls were made " + callsMade + " but none are terminating");
log.debug("The following funtion calls were made " + functionCallsMade + " but none are terminating");
}

}

// Since we have a general response, add message asking if there is anything else
Expand All @@ -284,32 +286,31 @@ private LexV2Response processGPT(LexV2Event lexRequest) {
return buildResponse(lexRequest, botResponse);
}


/**
* Response that will Lex to be done so some action can be performed at the Chime Level (hang up, transfer, MOH, etc.)
* Response that will tell Lex we are done so some action can be performed at the
* Chime Level (hang up, transfer, MOH, etc.)
*
* @param lexRequest
* @param transferNumber
* @param botResponse
* @return
*/
private LexV2Response buildTerminatingResponse(LexV2Event lexRequest, String function_name, Map<String,String> functionArgs, String botResponse) {
private LexV2Response buildTerminatingResponse(LexV2Event lexRequest, String function_name, Map<String, String> functionArgs, String botResponse) {

final var attrs = lexRequest.getSessionState().getSessionAttributes();

// The controller (Chime SAM Lambda) will grab this from the session, then transfer the call for us
// The controller (Chime SAM Lambda) will grab this from the session, then perform the terminating action
attrs.put("action", function_name);
attrs.put("bot_response", botResponse);
attrs.putAll(functionArgs);


// State to return
final var ss = SessionState.builder()
// Retain the current session attributes
// Send all Session Attrs
.withSessionAttributes(attrs)
// Send back Transfer Intent and let lex know that caller will fullfil it (namely Chime SMA Controller)
// We are always using Fallback, and let Lex know everything is fulfilled
.withIntent(Intent.builder().withName("FallbackIntent").withState("Fulfilled").build())
// Indicate the action as delegate, meaning lex won't fullfill, the caller will (Chime SMA Controller)
// Indicate we are closing things up, IE we are done here
.withDialogAction(DialogAction.builder().withType("Close").build())
.build();

Expand All @@ -321,8 +322,9 @@ private LexV2Response buildTerminatingResponse(LexV2Event lexRequest, String fun
}

/**
* General Response used to send back a message and Elicit Intent again at LEX. IE, we are sending back GPT
* response, and then waiting for Lex to collect speech and once again call us so we can send to GPT, effectively
* General Response used to send back a message and Elicit Intent again at
* LEX. IE, we are sending back GPT response, and then waiting for Lex to
* collect speech and once again call us so we can send to GPT, effectively
* looping until we call a terminating event like Quit or Transfer.
*
* @param lexRequest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
package cloud.cleo.squareup;

import cloud.cleo.squareup.enums.LexInputMode;
import static cloud.cleo.squareup.ChatGPTLambda.HANGUP_FUNCTION_NAME;
import static cloud.cleo.squareup.ChatGPTLambda.TRANSFER_FUNCTION_NAME;
import cloud.cleo.squareup.functions.AbstractFunction;
Expand Down Expand Up @@ -68,7 +69,7 @@ public ChatGPTSessionState(String phoneNumber, LexInputMode inputMode) {
sb.append("When the caller indicates they are done with the conversation, execute the ").append(HANGUP_FUNCTION_NAME).append(" function. ");

// Transferring
if (AbstractFunction.squareEnabled) {
if (AbstractFunction.isSquareEnabled()) {
sb.append("To transfer or speak with a employee that has a phone number, execute the ").append(TRANSFER_FUNCTION_NAME).append(" function. ");
sb.append("Do not provide callers employee phone numbers, you can use the phone numbers to execute the ").append(TRANSFER_FUNCTION_NAME).append(" function. ");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package cloud.cleo.squareup;
package cloud.cleo.squareup.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;
Expand All @@ -25,7 +25,8 @@ public enum ChannelPlatform {
SLACK("Slack"),
CONNECT("Connect"),
CONNECT_CHAT("Connect Chat"),
GENESYS_CLOUD("Genesys Cloud");
GENESYS_CLOUD("Genesys Cloud"),
UNKNOWN("No Platform Provided");

@Getter
private final String channel;
Expand All @@ -47,7 +48,7 @@ public static ChannelPlatform fromString(String channel) {
case "Genesys Cloud" ->
GENESYS_CLOUD;
default ->
throw new RuntimeException("Unknown Channel Plaform [" + channel + "]");
UNKNOWN;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package cloud.cleo.squareup;
package cloud.cleo.squareup.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package cloud.cleo.squareup.functions;

import cloud.cleo.squareup.ChannelPlatform;
import cloud.cleo.squareup.enums.ChannelPlatform;
import static cloud.cleo.squareup.enums.ChannelPlatform.UNKNOWN;
import static cloud.cleo.squareup.ChatGPTLambda.crtAsyncHttpClient;
import cloud.cleo.squareup.LexInputMode;
import cloud.cleo.squareup.enums.LexInputMode;
import com.amazonaws.services.lambda.runtime.events.LexV2Event;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.squareup.square.Environment;
Expand All @@ -14,6 +15,7 @@
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.CompletionException;
import java.util.function.Function;
import java.util.regex.Pattern;
import lombok.AccessLevel;
Expand Down Expand Up @@ -47,12 +49,28 @@ public abstract class AbstractFunction<T> implements Cloneable {

/**
* When user is interacting via Voice, we need the calling number to send
* SMS to them
* SMS to them.
*/
@Getter(AccessLevel.PROTECTED)
@Setter(AccessLevel.PRIVATE)
private String callingNumber;

/**
* The Channel that is being used to interact with Lex. Will be UNKNOWN if
* no channel is set (like from Lex Console or AWS CLI, etc.).
*/
@Getter(AccessLevel.PROTECTED)
@Setter(AccessLevel.PRIVATE)
private ChannelPlatform channelPlatform;


/**
* The Lex Session ID.
*/
@Getter(AccessLevel.PROTECTED)
@Setter(AccessLevel.PRIVATE)
private String sessionId;

/**
* Define here since used by most the of the functions
*/
Expand All @@ -61,15 +79,23 @@ public abstract class AbstractFunction<T> implements Cloneable {
.environment(Environment.valueOf(System.getenv("SQUARE_ENVIRONMENT")))
.build();

@Getter
public final static boolean squareEnabled;
private final static boolean squareEnabled;

static {
final var key = System.getenv("SQUARE_API_KEY");
final var loc = System.getenv("SQUARE_LOCATION_ID");

squareEnabled = !((loc == null || loc.isBlank() || loc.equalsIgnoreCase("DISABLED")) || (key == null || key.isBlank() || loc.equalsIgnoreCase("DISABLED")));
log.debug("Square Enabled check = " + squareEnabled);
log.debug("Square Enabled = " + squareEnabled);
}

/**
* Is Square enabled (API Key and Location ID set to something).
*
* @return
*/
public final static boolean isSquareEnabled() {
return squareEnabled;
}

/**
Expand Down Expand Up @@ -119,12 +145,15 @@ public static FunctionExecutor getFunctionExecuter(LexV2Event lexRequest) {
}

String callingNumber = null;
ChannelPlatform channelPlatform = UNKNOWN;

if (lexRequest.getRequestAttributes() != null) {
if (lexRequest.getRequestAttributes().containsKey("x-amz-lex:channels:platform")) {
final var platform = lexRequest.getRequestAttributes().get("x-amz-lex:channels:platform");
final var platformS = lexRequest.getRequestAttributes().get("x-amz-lex:channels:platform");
final var platform = ChannelPlatform.fromString(platformS);
log.debug("Requesting platform is " + platform);
switch (ChannelPlatform.fromString(platform)) {
channelPlatform = platform;
switch (platform) {
case CHIME ->
// For Chime we will pass in the calling number as Session Attribute callingNumber
callingNumber = lexRequest.getSessionState().getSessionAttributes() != null
Expand All @@ -140,14 +169,16 @@ public static FunctionExecutor getFunctionExecuter(LexV2Event lexRequest) {
log.debug("No channels platform set, request from Lex Console or CLI");
}

final var fromNumber = callingNumber;

final var inputMode = LexInputMode.fromString(lexRequest.getInputMode());
final var list = new LinkedList<AbstractFunction>();
final var sessionId = lexRequest.getSessionId();

functions.values().forEach(f -> {
for (var f : functions.values()) {
try {
final var func = (AbstractFunction) f.clone();
func.setCallingNumber(callingNumber);
func.setChannelPlatform(channelPlatform);
func.setSessionId(sessionId);
switch (inputMode) {
case TEXT -> {
if (func.isText()) {
Expand All @@ -160,11 +191,10 @@ public static FunctionExecutor getFunctionExecuter(LexV2Event lexRequest) {
}
}
}
func.setCallingNumber(fromNumber);
} catch (CloneNotSupportedException ex) {
log.error("Error cloning Functions", ex);
}
});
}
return new FunctionExecutor(list.stream().map(f -> f.getChatFunction()).toList());
}

Expand Down Expand Up @@ -274,6 +304,9 @@ protected boolean hasValidUSMobileNumber() {
default ->
false;
};
} catch (CompletionException e) {
log.error("Unhandled Error", e.getCause());
return false;
} catch (Exception e) {
log.error("Error making pinpoint call", e);
return false;
Expand Down Expand Up @@ -315,7 +348,7 @@ protected boolean isText() {
/**
* When this function is called, will this result in ending the current
* session and returning control back to Chime. IE, hang up, transfer, etc.
* This should all be voice related since you never terminated a text
* This should all be voice related since you never terminate a text
* session, lex will time it out based on it's setting.
*
* @return
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cloud.cleo.squareup.functions;

import static cloud.cleo.squareup.ChatGPTLambda.crtAsyncHttpClient;
import java.util.concurrent.CompletionException;
import java.util.function.Function;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.sns.SnsAsyncClient;
Expand Down Expand Up @@ -45,6 +46,9 @@ protected Function getExecutor() {
final var result = snsAsyncClient.publish(b -> b.phoneNumber(callingNumber).message(DRIVING_DIRECTIONS_URL) ).join();
log.info("SMS Directions sent to " + callingNumber + " with SNS id of " + result.messageId());
return mapper.createObjectNode().put("status","SUCCESS").put("message", "The directions have been sent");
} catch (CompletionException e) {
log.error("Could not send Directions via SMS to caller",e.getCause());
return mapper.createObjectNode().put("status","FAILED").put("message", "An error has occurred, this function may be down");
} catch (Exception e) {
log.error("Could not send Directions via SMS to caller",e);
return mapper.createObjectNode().put("status","FAILED").put("message", "An error has occurred, this function may be down");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ private static class Request {
*/
@Override
protected boolean isEnabled() {
return squareEnabled;
return isSquareEnabled();
}

}
Loading

0 comments on commit 7a35543

Please sign in to comment.