-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathalexa.fsx
325 lines (279 loc) · 7.37 KB
/
alexa.fsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
#r "node_modules/fable-core/Fable.Core.dll"
#load "lambda.fsx"
open Fable.Core
open Fable.Core.JsInterop
[<StringEnum>]
type SessionEndedReason =
| [<CompiledName("USER_INITIATED")>] UserInitiated
| [<CompiledName("ERROR")>] Error
| [<CompiledName("EXCEEDED_MAX_REPROMPTS")>] ExceededMaxReprompts
module Interop =
[<Pojo>]
type User =
{
userId : string
accessToken : string option
}
[<Pojo>]
type Application =
{
applicationId : string
}
[<Pojo>]
type Session =
{
``new`` : bool
sessionId : string
attributes : obj option
application : Application
user : User
}
[<StringEnum>]
type RequestType =
| [<CompiledName("LaunchRequest")>] LaunchRequest
| [<CompiledName("IntentRequest")>] IntentRequest
| [<CompiledName("SessionEndedRequest")>] SessionEndedRequest
[<Pojo>]
type Intent =
{
name : string
slots : obj option
}
[<Pojo>]
type RequestBody =
{
requestId : string
timeStamp : string
``type`` : RequestType
reason : SessionEndedReason option
intent : Intent option
}
[<Pojo>]
type Request =
{
version : string
session : Session
request : RequestBody
}
[<StringEnum>]
type SpeechType =
| [<CompiledName("PlainText")>] PlainText
| [<CompiledName("SSML")>] SSML
type Speech =
{
``type`` : SpeechType
text : string option
ssml : string option
}
type Reprompt =
{
outputSpeech : Speech
}
[<StringEnum>]
type CardType =
| [<CompiledName("Simple")>] Simple
| [<CompiledName("Standard")>] Standard
| [<CompiledName("LinkAccount")>] LinkAccount
type Image =
{
smallImageUrl : string
largeImageUrl : string
}
type Card =
{
``type`` : CardType
title : string option
content : string option
text : string option
image : Image option
}
type ResponseBody =
{
shouldEndSession : bool
outputSpeech : Speech option
reprompt : Reprompt option
card : Card option
}
type Response =
{
version : string
sessionAttributes : obj
response : ResponseBody
}
type Slots = Map<string, string>
type Request =
| Launch
| Intent of name : string * Slots
| SessionEnded of SessionEndedReason
type Session<'a> =
{
ApplicationId : string
UserId : string
UserAccessToken : string option
SessionId : string
IsNew : bool
Attributes : 'a
Raw : Interop.Request
}
let attributesKey = "AlexaFs"
module Request =
[<Emit("Object.keys($0)")>]
let objKeys obj : string[] = jsNative
let ofRawRequest (initialAttributes : 'a) (request : Interop.Request) : Request * Session<'a> =
let parsedRequest =
match request.request.``type`` with
| Interop.RequestType.LaunchRequest -> Launch
| Interop.RequestType.IntentRequest ->
let intent = request.request.intent.Value
let slotKeys =
match intent.slots with
| None -> [||]
| Some slots -> objKeys slots
let slots =
slotKeys
|> Array.map(fun key ->
let slot = intent.slots?(key)
!!slot?name, !!slot?value
)
|> Map.ofArray
Intent(intent.name, slots)
| Interop.RequestType.SessionEndedRequest -> SessionEnded(request.request.reason.Value)
let attributes = request.session.attributes |> Option.bind(fun attributes -> !!attributes?(attributesKey))
let session =
{
ApplicationId = request.session.application.applicationId
UserId = request.session.user.userId
UserAccessToken = request.session.user.accessToken
SessionId = request.session.sessionId
IsNew = request.session.``new``
Attributes = defaultArg attributes initialAttributes
Raw = request
}
parsedRequest, session
type Speech =
| Text of string
| SSML of string
type Image =
{
SmallUrl : string
LargeUrl : string
}
type CustomCard =
{
Title : string
Content : string
Image : Image option
}
type Card =
| LinkAccount
| Custom of CustomCard
type Response =
{
EndSession : bool
Speech : Speech option
Reprompt : Speech option
Card : Card option
}
module Response =
let private makeResponse sessionAttributes response : Interop.Response =
{
version = "1.0.0"
sessionAttributes = createObj [ attributesKey ==> sessionAttributes ]
response = response
}
let private makeSpeech speech : Interop.Speech =
match speech with
| Text text ->
{
``type`` = Interop.SpeechType.PlainText
text = Some text
ssml = None
}
| SSML ssml ->
{
``type`` = Interop.SpeechType.SSML
text = None
ssml = Some ssml
}
let private makeImage image : Interop.Image =
{
smallImageUrl = image.SmallUrl
largeImageUrl = image.LargeUrl
}
let private defaultCard : Interop.Card =
{
``type`` = Interop.CardType.Simple
title = None
content = None
text = None
image = None
}
let private makeCard card =
match card with
| LinkAccount ->
{
defaultCard with
``type`` = Interop.CardType.LinkAccount
}
| Custom details ->
match details.Image with
| None ->
{ defaultCard with
``type`` = Interop.CardType.Simple
title = Some details.Title
content = Some details.Content
}
| Some image ->
{
defaultCard with
``type`` = Interop.CardType.Standard
title = Some details.Title
text = Some details.Content
image = Some (makeImage image)
}
let toRawResponse response attributes =
makeResponse
attributes
{
outputSpeech = response.Speech |> Option.map makeSpeech
reprompt = response.Reprompt |> Option.map (fun reprompt -> { outputSpeech = makeSpeech reprompt })
shouldEndSession = response.EndSession
card = response.Card |> Option.map makeCard
}
let empty =
{
EndSession = false
Speech = None
Reprompt = None
Card = None
}
/// Immediately exit without speech
let exit =
{ empty with EndSession = true }
let withSpeech speech response =
{ response with Speech = Some speech }
let say speech =
{ empty with Speech = Some speech }
let withReprompt speech response =
{ response with Reprompt = Some speech }
let endSession response =
{ response with EndSession = true }
let withCard card response =
{ response with Card = Some card }
let withCustomCard card response =
{ response with Card = Some (Custom card) }
let withLinkAccount response =
{ response with Card = Some LinkAccount }
let linkAccount speechOption =
{ empty with Card = Some LinkAccount }
type Handler<'a> = Request -> Session<'a> -> Async<Response * 'a>
let lambda (defaultAttributes : 'a, handler : Handler<'a>) : Lambda.NativeHandler<Interop.Request, Interop.Response option> =
Lambda.handler (fun context request -> async {
let request, session = Request.ofRawRequest defaultAttributes request
let! response, attributes = handler request session
match request with
| SessionEnded _ ->
return None
| _ ->
return Some <| Response.toRawResponse response attributes
})