Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions events/example_lexv2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package events_test

import (
"fmt"

"github.com/aws/aws-lambda-go/events"
)

func ExampleLexV2Event() {
event := events.LexV2Event{
MessageVersion: "1.0",
InvocationSource: "FulfillmentCodeHook",
InputMode: "Text",
SessionID: "12345678-1234-1234-1234-123456789012",
InputTranscript: "check my balance",
Bot: events.LexV2Bot{
ID: "BOTID",
Name: "BankingBot",
AliasID: "ALIASID",
LocaleID: "en_US",
Version: "1",
},
SessionState: events.LexV2SessionState{
Intent: &events.LexV2Intent{
Name: "CheckBalance",
State: "ReadyForFulfillment",
ConfirmationState: "None",
Slots: map[string]events.LexV2Slot{},
},
},
}

fmt.Printf("Bot: %s, Intent: %s\n", event.Bot.Name, event.SessionState.Intent.Name)
// Output: Bot: BankingBot, Intent: CheckBalance
}

func ExampleLexV2Response() {
response := events.LexV2Response{
SessionState: events.LexV2SessionState{
DialogAction: &events.LexV2DialogAction{
Type: "Close",
},
Intent: &events.LexV2Intent{
Name: "CheckBalance",
State: "Fulfilled",
ConfirmationState: "None",
Slots: map[string]events.LexV2Slot{},
},
},
Messages: []events.LexV2Message{
{
ContentType: "PlainText",
Content: "Your balance is $1,234.56",
},
},
}

fmt.Printf("Action: %s, Message: %s\n", response.SessionState.DialogAction.Type, response.Messages[0].Content)
// Output: Action: Close, Message: Your balance is $1,234.56
}
189 changes: 189 additions & 0 deletions events/lexv2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package events

// Amazon Lex V2 event structures for Lambda functions.
//
// Lex V2 provides a conversational interface for building chatbots and voice assistants.
// These structures represent the input events sent from Lex V2 to Lambda functions
// and the expected response format.
//
// Key differences from Lex V1:
// - sessionId instead of userId
// - interpretations array with confidence scores
// - enhanced slot structure with shape and values
// - sessionState replaces separate sessionAttributes
// - dialogAction moved into sessionState
//
// Note: This library uses plain string types for simplicity. For type-safe constants
// and validation, refer to the AWS documentation for valid values:
// https://docs.aws.amazon.com/lexv2/latest/dg/lambda-input-format.html
// https://docs.aws.amazon.com/lexv2/latest/dg/lambda-response-format.html
//
// For more information, see:
// https://docs.aws.amazon.com/lexv2/latest/dg/lambda.html

// LexV2Event represents the input event from Amazon Lex V2 to Lambda.
// https://docs.aws.amazon.com/lexv2/latest/dg/lambda-input-format.html
type LexV2Event struct {
MessageVersion string `json:"messageVersion"`
InvocationSource string `json:"invocationSource"`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would recommend a data type for InvocationSource:

// InvocationSource represents the source of the invocation within the Lex Bot lifecycle.
type LexInvocationSource string

const (
	LexInvocationSourceDialogCodeHook      InvocationSource = "DialogCodeHook"
	LexInvocationSourceFulfillmentCodeHook InvocationSource = "FulfillmentCodeHook"
)
Suggested change
InvocationSource string `json:"invocationSource"`
InvocationSource LexInvocationSource `json:"invocationSource"`

InputMode string `json:"inputMode"`
Copy link

@aaiezza aaiezza Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would recommend a data type for InputMode:

// InputMode represents the mode of innput from the user.
type InputMode string

const (
	InputModeText  InputMode = "Text"
	InputModeVoice InputMode = "Voice"
	InputModeDTMF  InputMode = "DTMF"
)
Suggested change
InputMode string `json:"inputMode"`
InputMode InputMode `json:"inputMode"`

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

intentionally kept these as plain string types to match the pattern used throughout the aws-lambda-go events library (see lex.go, cognito.go, apigw.go)

AWS can add new enum values without requiring library updates. Developers are pointed to the official AWS docs for valid values if they want to add type-safe constants in their own code.

https://docs.aws.amazon.com/lexv2/latest/dg/lambda-input-format.html
https://docs.aws.amazon.com/lexv2/latest/dg/lambda-response-format.html

This approach keeps the library minimal while remaining flexible as AWS evolves the service.

ResponseContentType string `json:"responseContentType"`
SessionID string `json:"sessionId"`
InputTranscript string `json:"inputTranscript,omitempty"`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The LexV2Event struct is missing the optional InvocationLabel:

Suggested change
InputTranscript string `json:"inputTranscript,omitempty"`
InputTranscript string `json:"inputTranscript"`
InvocationLabel *string `json:"invocationLabel,omitempty"`

The documentation does not describes this field as optional, but testing has shown that the field is optional, so it can be represented as a string pointer.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is InputTranscript actually optional? If so, it could be a pointer, so that the json struct tag omitempty, results in a nil value instead of a default empty string.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks will add InvocationLabel in next commit

InputTranscript: The documentation not explicitly mark inputTranscript as required. However, I'm keeping it as a regular string with omitempty because it represents user input and is present in all standard DialogCodeHook and FulfillmentCodeHook invocations. It's a core field fundamental to Lex's operation. Making it a pointer would incorrectly signal it's rarely used, when it's present in nearly every event. The omitempty tag handles edge cases where it might be absent. While the docs don't say "required" verbatim, the field's purpose and typical usage patterns indicate it's a standard field rather than optional.

InvocationLabel *string `json:"invocationLabel,omitempty"`
Bot LexV2Bot `json:"bot"`
Interpretations []LexV2Interpretation `json:"interpretations"`
ProposedNextState *LexV2ProposedNextState `json:"proposedNextState,omitempty"`
RequestAttributes map[string]string `json:"requestAttributes"`
SessionState LexV2SessionState `json:"sessionState"`
Transcriptions []LexV2Transcription `json:"transcriptions,omitempty"`
}

type LexV2Bot struct {
ID string `json:"id"`
Name string `json:"name"`
AliasID string `json:"aliasId"`
AliasName string `json:"aliasName,omitempty"`
LocaleID string `json:"localeId"`
Version string `json:"version"`
}

type LexV2Interpretation struct {
Intent LexV2Intent `json:"intent"`
InterpretationSource string `json:"interpretationSource,omitempty"`
NLUConfidence *LexV2NLUConfidence `json:"nluConfidence,omitempty"`
SentimentResponse *LexV2SentimentResponse `json:"sentimentResponse,omitempty"`
}

type LexV2NLUConfidence struct {
Score float64 `json:"score,omitempty"`
}

type LexV2Intent struct {
ConfirmationState string `json:"confirmationState"`
Name string `json:"name"`
Slots map[string]LexV2Slot `json:"slots"`
State string `json:"state"`
KendraResponse *LexV2KendraResponse `json:"kendraResponse,omitempty"`
}

type LexV2Slot struct {
Shape string `json:"shape,omitempty"`
Value *LexV2SlotValue `json:"value,omitempty"`
Values []LexV2SlotValue `json:"values,omitempty"`
}

type LexV2SlotValue struct {
OriginalValue string `json:"originalValue"`
InterpretedValue string `json:"interpretedValue"`
ResolvedValues []string `json:"resolvedValues,omitempty"`
}

// LexV2KendraResponse contains information about the results of a Kendra search query.
// This field only appears if the intent is a KendraSearchIntent.
// For the complete structure, see:
// https://docs.aws.amazon.com/kendra/latest/dg/API_Query.html#API_Query_ResponseSyntax
type LexV2KendraResponse struct {
// The response structure matches the Kendra Query API response.
// Using interface{} to accommodate the full Kendra response without
// duplicating the entire Kendra API structure here.
// Users should unmarshal this to the appropriate Kendra types if needed.
}

type LexV2SentimentResponse struct {
Sentiment string `json:"sentiment"`
SentimentScore LexV2SentimentScore `json:"sentimentScore"`
}

type LexV2SentimentScore struct {
Mixed float64 `json:"mixed"`
Negative float64 `json:"negative"`
Neutral float64 `json:"neutral"`
Positive float64 `json:"positive"`
}

type LexV2ProposedNextState struct {
DialogAction *LexV2DialogAction `json:"dialogAction,omitempty"`
Intent *LexV2Intent `json:"intent,omitempty"`
}

type LexV2SessionState struct {
ActiveContexts []LexV2ActiveContext `json:"activeContexts,omitempty"`
SessionAttributes map[string]string `json:"sessionAttributes,omitempty"`
RuntimeHints *LexV2RuntimeHints `json:"runtimeHints,omitempty"`
DialogAction *LexV2DialogAction `json:"dialogAction,omitempty"`
Intent *LexV2Intent `json:"intent,omitempty"`
OriginatingRequestID string `json:"originatingRequestId,omitempty"`
}

type LexV2ActiveContext struct {
Name string `json:"name"`
ContextAttributes map[string]string `json:"contextAttributes"`
TimeToLive LexV2TimeToLive `json:"timeToLive"`
}

type LexV2TimeToLive struct {
TimeToLiveInSeconds int `json:"timeToLiveInSeconds"`
TurnsToLive int `json:"turnsToLive"`
}

type LexV2RuntimeHints struct {
SlotHints map[string]map[string]LexV2SlotHint `json:"slotHints,omitempty"`
}

type LexV2SlotHint struct {
RuntimeHintValues []LexV2RuntimeHintValue `json:"runtimeHintValues"`
}

type LexV2RuntimeHintValue struct {
Phrase string `json:"phrase"`
}

type LexV2Transcription struct {
Transcription string `json:"transcription"`
TranscriptionConfidence float64 `json:"transcriptionConfidence,omitempty"`
ResolvedContext *LexV2ResolvedContext `json:"resolvedContext,omitempty"`
ResolvedSlots map[string]LexV2Slot `json:"resolvedSlots,omitempty"`
}

type LexV2ResolvedContext struct {
Intent string `json:"intent"`
}

// LexV2Response represents the response from Lambda to Lex V2.
// https://docs.aws.amazon.com/lexv2/latest/dg/lambda-response-format.html
type LexV2Response struct {
SessionState LexV2SessionState `json:"sessionState"`
Messages []LexV2Message `json:"messages,omitempty"`
RequestAttributes map[string]string `json:"requestAttributes,omitempty"`
}

type LexV2DialogAction struct {
Type string `json:"type"`
SlotToElicit string `json:"slotToElicit,omitempty"`
SlotElicitationStyle string `json:"slotElicitationStyle,omitempty"`
SubSlotToElicit *LexV2SubSlotToElicit `json:"subSlotToElicit,omitempty"`
}

type LexV2SubSlotToElicit struct {
Name string `json:"name"`
SubSlotToElicit *LexV2SubSlotToElicit `json:"subSlotToElicit,omitempty"`
}

type LexV2Message struct {
ContentType string `json:"contentType"`
Content string `json:"content,omitempty"`
ImageResponseCard *LexV2ImageResponseCard `json:"imageResponseCard,omitempty"`
}

type LexV2ImageResponseCard struct {
Title string `json:"title"`
Subtitle string `json:"subtitle,omitempty"`
ImageURL string `json:"imageUrl,omitempty"`
Buttons []LexV2Button `json:"buttons,omitempty"`
}

type LexV2Button struct {
Text string `json:"text"`
Value string `json:"value"`
}
49 changes: 49 additions & 0 deletions events/lexv2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package events

import (
"encoding/json"
"testing"

"github.com/aws/aws-lambda-go/events/test"
"github.com/stretchr/testify/assert"
)

func TestLexV2EventMarshaling(t *testing.T) {
inputJSON := test.ReadJSONFromFile(t, "./testdata/lexv2-event.json")

var inputEvent LexV2Event
if err := json.Unmarshal(inputJSON, &inputEvent); err != nil {
t.Errorf("could not unmarshal event. details: %v", err)
}

outputJSON, err := json.Marshal(inputEvent)
if err != nil {
t.Errorf("could not marshal event. details: %v", err)
}

assert.JSONEq(t, string(inputJSON), string(outputJSON))
}

func TestLexV2ResponseMarshaling(t *testing.T) {
inputJSON := test.ReadJSONFromFile(t, "./testdata/lexv2-response.json")

var inputEvent LexV2Response
if err := json.Unmarshal(inputJSON, &inputEvent); err != nil {
t.Errorf("could not unmarshal event. details: %v", err)
}

outputJSON, err := json.Marshal(inputEvent)
if err != nil {
t.Errorf("could not marshal event. details: %v", err)
}

assert.JSONEq(t, string(inputJSON), string(outputJSON))
}

func TestLexV2MarshalingMalformedJson(t *testing.T) {
test.TestMalformedJson(t, LexV2Event{})
}

func TestLexV2ResponseMalformedJson(t *testing.T) {
test.TestMalformedJson(t, LexV2Response{})
}
60 changes: 60 additions & 0 deletions events/testdata/lexv2-event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"messageVersion": "1.0",
"invocationSource": "FulfillmentCodeHook",
"inputMode": "Text",
"responseContentType": "text/plain; charset=utf-8",
"sessionId": "12345678-1234-1234-1234-123456789012",
"inputTranscript": "I want to check my balance",
"bot": {
"id": "BOTID",
"name": "BankingBot",
"aliasId": "ALIASID",
"aliasName": "Prod",
"localeId": "en_US",
"version": "1"
},
"interpretations": [
{
"intent": {
"confirmationState": "None",
"name": "CheckBalance",
"slots": {
"accountType": {
"value": {
"originalValue": "checking",
"interpretedValue": "checking",
"resolvedValues": ["CHECKING"]
}
}
},
"state": "ReadyForFulfillment"
},
"nluConfidence": {
"score": 0.95
}
}
],
"requestAttributes": {
"x-amz-lex:channel-type": "Twilio-SMS"
},
"sessionState": {
"sessionAttributes": {
"customerId": "12345"
},
"intent": {
"confirmationState": "None",
"name": "CheckBalance",
"slots": {
"accountType": {
"value": {
"originalValue": "checking",
"interpretedValue": "checking",
"resolvedValues": ["CHECKING"]
}
}
},
"state": "ReadyForFulfillment"
},
"originatingRequestId": "request-123"
}
}
30 changes: 30 additions & 0 deletions events/testdata/lexv2-response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"sessionState": {
"dialogAction": {
"type": "Close"
},
"intent": {
"name": "CheckBalance",
"slots": {
"accountType": {
"value": {
"originalValue": "checking",
"interpretedValue": "checking",
"resolvedValues": ["CHECKING"]
}
}
},
"state": "Fulfilled",
"confirmationState": "None"
},
"sessionAttributes": {
"customerId": "12345"
}
},
"messages": [
{
"contentType": "PlainText",
"content": "Your checking account balance is $1,234.56"
}
]
}
Loading