Skip to content

Commit

Permalink
Add Pinpoint SMS as a Channel
Browse files Browse the repository at this point in the history
  • Loading branch information
docwho2 committed Jan 2, 2024
1 parent c1f1da4 commit d989532
Show file tree
Hide file tree
Showing 7 changed files with 314 additions and 66 deletions.
36 changes: 3 additions & 33 deletions ChatGPT/src/main/java/cloud/cleo/squareup/ChatGPTLambda.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@
import cloud.cleo.squareup.json.ZoneIdDeserializer;
import cloud.cleo.squareup.json.ZonedSerializer;
import static cloud.cleo.squareup.lang.LangUtil.LanguageIds.*;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.LexV2Event;
import com.amazonaws.services.lambda.runtime.events.LexV2Event.Intent;
import com.amazonaws.services.lambda.runtime.events.LexV2Event.SessionState;
import com.amazonaws.services.lambda.runtime.events.LexV2Response;
Expand All @@ -38,7 +35,6 @@
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 All @@ -53,7 +49,7 @@
*
* @author sjensen
*/
public class ChatGPTLambda implements RequestHandler<LexV2Event, LexV2Response> {
public abstract class ChatGPTLambda {

// Initialize the Log4j logger.
Logger log = LogManager.getLogger(ChatGPTLambda.class);
Expand Down Expand Up @@ -106,34 +102,8 @@ public class ChatGPTLambda implements RequestHandler<LexV2Event, LexV2Response>
new FaceBookOperations();
}

@Override
public LexV2Response handleRequest(LexV2Event lexRequest, Context cntxt) {
// Wrapped Event Class
final LexV2EventWrapper event = new LexV2EventWrapper(lexRequest);
try {
log.debug(mapper.valueToTree(lexRequest).toPrettyString());
// Intent which doesn't matter for us
log.debug("Intent: " + event.getIntent());

// For this use case, we only ever get the FallBack Intent, so the intent name means nothing here
// We will process everythiung coming in as text to pass to GPT
// IE, we are only using lex here to process speech and send it to us
return switch (event.getIntent()) {
default ->
processGPT(event);
};

} catch (CompletionException e) {
log.error("Unhandled Future Exception", e.getCause());
return buildResponse(new LexV2EventWrapper(lexRequest), event.getLangString(UNHANDLED_EXCEPTION));
} catch (Exception e) {
log.error("Unhandled Exception", e);
// Unhandled Exception
return buildResponse(new LexV2EventWrapper(lexRequest), event.getLangString(UNHANDLED_EXCEPTION));
}
}

private LexV2Response processGPT(LexV2EventWrapper lexRequest) {
protected LexV2Response processGPT(LexV2EventWrapper lexRequest) {
var input = lexRequest.getInputTranscript();
final var attrs = lexRequest.getSessionAttributes();
// Will be phone if from SMS, Facebook the Page Scoped userID, Chime unique generated ID
Expand Down Expand Up @@ -393,7 +363,7 @@ private LexV2Response buildResponse(LexV2EventWrapper lexRequest, String respons
* @param response
* @return
*/
private LexV2Response buildResponse(LexV2EventWrapper lexRequest, String response) {
protected LexV2Response buildResponse(LexV2EventWrapper lexRequest, String response) {
return buildResponse(lexRequest, response, null);
}

Expand Down
57 changes: 57 additions & 0 deletions ChatGPT/src/main/java/cloud/cleo/squareup/ChatGPTLambdaLex.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package cloud.cleo.squareup;

import static cloud.cleo.squareup.ChatGPTLambda.mapper;
import static cloud.cleo.squareup.lang.LangUtil.LanguageIds.UNHANDLED_EXCEPTION;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.LexV2Event;
import com.amazonaws.services.lambda.runtime.events.LexV2Response;
import java.util.concurrent.CompletionException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;





/**
* Process incoming Lex Events
*
*
* @author sjensen
*/
public class ChatGPTLambdaLex extends ChatGPTLambda implements RequestHandler<LexV2Event, LexV2Response> {

// Initialize the Log4j logger.
Logger log = LogManager.getLogger(ChatGPTLambdaLex.class);

@Override
public LexV2Response handleRequest(LexV2Event lexRequest, Context cntxt) {
// Wrapped Event Class
final LexV2EventWrapper event = new LexV2EventWrapper(lexRequest);
try {
log.debug(mapper.valueToTree(lexRequest).toPrettyString());
// Intent which doesn't matter for us
log.debug("Intent: " + event.getIntent());

// For this use case, we only ever get the FallBack Intent, so the intent name means nothing here
// We will process everythiung coming in as text to pass to GPT
// IE, we are only using lex here to process speech and send it to us
return switch (event.getIntent()) {
default ->
processGPT(event);
};

} catch (CompletionException e) {
log.error("Unhandled Future Exception", e.getCause());
return buildResponse(new LexV2EventWrapper(lexRequest), event.getLangString(UNHANDLED_EXCEPTION));
} catch (Exception e) {
log.error("Unhandled Exception", e);
// Unhandled Exception
return buildResponse(new LexV2EventWrapper(lexRequest), event.getLangString(UNHANDLED_EXCEPTION));
}
}



}
113 changes: 113 additions & 0 deletions ChatGPT/src/main/java/cloud/cleo/squareup/ChatGPTLambdaPinpoint.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package cloud.cleo.squareup;

import static cloud.cleo.squareup.lang.LangUtil.LanguageIds.UNHANDLED_EXCEPTION;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.LexV2Response;
import com.amazonaws.services.lambda.runtime.events.SNSEvent;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.concurrent.CompletionException;
import lombok.Data;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.sns.SnsAsyncClient;

/**
* Process incoming SMS from Pinpoint and respond as another Channel Type.
*
* https://docs.aws.amazon.com/sms-voice/latest/userguide/phone-numbers-two-way-sms.html
*
* @author sjensen
*/
public class ChatGPTLambdaPinpoint extends ChatGPTLambda implements RequestHandler<SNSEvent, Void> {

// Initialize the Log4j logger.
Logger log = LogManager.getLogger(ChatGPTLambdaPinpoint.class);

final static SnsAsyncClient snsAsyncClient = SnsAsyncClient.builder()
// Force SMS sending to east because that's where all the 10DLC and campaign crap setup is done
// Otherwise have to pay for registrations and numbers in 2 regions, HUGE HASSLE (and more monthly cost)
// Also then all texts are sourced from the same phone number for consistancy
.region(Region.US_EAST_1)
.httpClient(crtAsyncHttpClient)
.build();

@Override
public Void handleRequest(SNSEvent input, Context cntxt) {
// Only 1 record is every presented
SNSEvent.SNS snsEvent = input.getRecords().get(0).getSns();
log.debug("Recieved SNS Event" + snsEvent);

// Convert payload to Pinpoint Event
final var ppe = mapper.convertValue(snsEvent, PinpointEvent.class);

// Wrapped Event Class
final LexV2EventWrapper event = new LexV2EventWrapper(ppe);
LexV2Response response;
try {
response = processGPT(event);
} catch (CompletionException e) {
log.error("Unhandled Future Exception", e.getCause());
response = buildResponse(event, event.getLangString(UNHANDLED_EXCEPTION));
} catch (Exception e) {
log.error("Unhandled Exception", e);
// Unhandled Exception
response = buildResponse(event, event.getLangString(UNHANDLED_EXCEPTION));
}

// Take repsonse body message from the LexV2Reponse and respond to SMS via SNS
final var botResponse = response.getMessages()[0].getContent();
final var result = snsAsyncClient.publish(b -> b.phoneNumber(ppe.getOriginationNumber())
.message(botResponse))
.join();
log.info("SMS Bot Response sent to " + ppe.getOriginationNumber() + " with SNS id of " + result.messageId());

return null;
}

/**
* The SNS payload we will receive from Incoming SMS messages.
*
*/
@Data
public static class PinpointEvent {

/**
* The phone number that sent the incoming message to you (in other words, your customer's phone number).
*/
@JsonProperty(value = "originationNumber")
private String originationNumber;

/**
* The phone number that the customer sent the message to (your dedicated phone number).
*/
@JsonProperty(value = "destinationNumber")
private String destinationNumber;

/**
* The registered keyword that's associated with your dedicated phone number.
*/
@JsonProperty(value = "messageKeyword")
private String messageKeyword;

/**
* The message that the customer sent to you.
*/
@JsonProperty(value = "messageBody")
private String messageBody;

/**
* The unique identifier for the incoming message.
*/
@JsonProperty(value = "inboundMessageId")
private String inboundMessageId;

/**
* The unique identifier of the message that the customer is responding to.
*/
@JsonProperty(value = "previousPublishedMessageId")
private String previousPublishedMessageId;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public ChatGPTSessionState(LexV2EventWrapper lexRequest) {
sb.append("The user's name is ").append(name).append(". Please greet the user by name and personalize responses when appropiate. ");
}
}
case TWILIO -> {
case TWILIO,PINPOINT -> {
// Try and keep SMS segements down, hence the "very" short reference and character preference
sb.append("The user is interacting via SMS. Please keep answers very short and concise, preferably under 180 characters. ");

Expand Down
57 changes: 40 additions & 17 deletions ChatGPT/src/main/java/cloud/cleo/squareup/LexV2EventWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
*/
package cloud.cleo.squareup;

import cloud.cleo.squareup.ChatGPTLambdaPinpoint.PinpointEvent;
import cloud.cleo.squareup.enums.*;
import static cloud.cleo.squareup.enums.ChannelPlatform.*;
import cloud.cleo.squareup.lang.LangUtil;
import cloud.cleo.squareup.lang.LangUtil.LanguageIds;
import com.amazonaws.services.lambda.runtime.events.LexV2Event;
import com.amazonaws.services.lambda.runtime.events.LexV2Event.Bot;
import java.util.Locale;
import java.util.Map;
import lombok.AccessLevel;
Expand All @@ -21,44 +23,65 @@
*/
public class LexV2EventWrapper {

/**
/**
* The underlying Lex V2 Event.
*/
@Getter(AccessLevel.PUBLIC)
private final LexV2Event event;

/**
* Language Support.
*/
@Getter(AccessLevel.PUBLIC)
private final LangUtil lang;



/**
* Wrap a Normal LexV2Event.
*
* @param event
*/
public LexV2EventWrapper(LexV2Event event) {
this.event = event;
this.lang = new LangUtil(event.getBot().getLocaleId());
}


/**
* Turn a Pinpoint Event into a Wrapped LexEvent.
*
* @param ppe
*/
public LexV2EventWrapper(PinpointEvent ppe) {
this(LexV2Event.builder()
.withInputMode(LexInputMode.TEXT.getMode())
// Exclude + from the E164 to be consistant with Twilio (shouldn't use + in sessionID)
.withSessionId(ppe.getOriginationNumber().substring(1))
// Mimic Platform input type of Pinpoint
.withRequestAttributes(Map.of("x-amz-lex:channels:platform", ChannelPlatform.PINPOINT.getChannel()))
// The incoming SMS body will be in the input Transcript
.withInputTranscript(ppe.getMessageBody())
// SMS has no locale target, just use en_US
.withBot(Bot.builder().withLocaleId("en_US").build())
.build());
}

/**
* The Java Locale for this Bot request.
*
* @return
*
* @return
*/
public Locale getLocale() {
return lang.getLocale();
}

/**
* Get a Language Specific String.
*
*
* @param id
* @return
* @return
*/
public String getLangString(LanguageIds id) {
return lang.getString(id);
}


/**
* Return Input Mode as Enumeration.
Expand Down Expand Up @@ -111,8 +134,8 @@ public ChannelPlatform getChannelPlatform() {
}

/**
* Get the calling (or SMS originating) number for the session. For Channels
* like Facebook or CLI testing, this will not be available and null.
* Get the calling (or SMS originating) number for the session. For Channels like Facebook or CLI testing, this will
* not be available and null.
*
* @return E164 number or null if not applicable to channel.
*/
Expand All @@ -122,7 +145,7 @@ public String getPhoneE164() {
// For Chime we will pass in the calling number as Session Attribute callingNumber
event.getSessionState().getSessionAttributes() != null
? event.getSessionState().getSessionAttributes().get("callingNumber") : null;
case TWILIO ->
case TWILIO, PINPOINT ->
// Twilio channel will use sessiond ID, however without +, so prepend to make it full E164
"+".concat(event.getSessionId());
default ->
Expand All @@ -138,10 +161,11 @@ public String getPhoneE164() {
public String getInputTranscript() {
return event.getInputTranscript();
}

/**
* The Intent for this request.
* @return
*
* @return
*/
public String getIntent() {
return event.getSessionState().getIntent().getName();
Expand All @@ -164,7 +188,6 @@ public Map<String, String> getSessionAttributes() {
public String getSessionId() {
return event.getSessionId();
}


/**
* Is this request from the Facebook Channel.
Expand Down
Loading

0 comments on commit d989532

Please sign in to comment.