Skip to content
Navigation Menu
{{ message }}
-
Notifications
You must be signed in to change notification settings - Fork 157
Expand file tree
/
Copy pathPracticum-rejoinders.html
More file actions
480 lines (480 loc) · 21.2 KB
/
Copy pathPracticum-rejoinders.html
File metadata and controls
480 lines (480 loc) · 21.2 KB
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
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
<head>
<meta charset="utf-8" />
<meta name="generator" content="pandoc" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
<title>Practicum-rejoinders</title>
<style>
html {
color: #1a1a1a;
background-color: #fdfdfd;
}
body {
margin: 0 auto;
max-width: 36em;
padding-left: 50px;
padding-right: 50px;
padding-top: 50px;
padding-bottom: 50px;
hyphens: auto;
overflow-wrap: break-word;
text-rendering: optimizeLegibility;
font-kerning: normal;
}
@media (max-width: 600px) {
body {
font-size: 0.9em;
padding: 12px;
}
h1 {
font-size: 1.8em;
}
}
@media print {
html {
background-color: white;
}
body {
background-color: transparent;
color: black;
font-size: 12pt;
}
p, h2, h3 {
orphans: 3;
widows: 3;
}
h2, h3, h4 {
page-break-after: avoid;
}
}
p {
margin: 1em 0;
}
a {
color: #1a1a1a;
}
a:visited {
color: #1a1a1a;
}
img {
max-width: 100%;
}
svg {
height; auto;
max-width: 100%;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 1.4em;
}
h5, h6 {
font-size: 1em;
font-style: italic;
}
h6 {
font-weight: normal;
}
ol, ul {
padding-left: 1.7em;
margin-top: 1em;
}
li > ol, li > ul {
margin-top: 0;
}
blockquote {
margin: 1em 0 1em 1.7em;
padding-left: 1em;
border-left: 2px solid #e6e6e6;
color: #606060;
}
code {
font-family: Menlo, Monaco, Consolas, 'Lucida Console', monospace;
font-size: 85%;
margin: 0;
hyphens: manual;
}
pre {
margin: 1em 0;
overflow: auto;
}
pre code {
padding: 0;
overflow: visible;
overflow-wrap: normal;
}
.sourceCode {
background-color: transparent;
overflow: visible;
}
hr {
background-color: #1a1a1a;
border: none;
height: 1px;
margin: 1em 0;
}
table {
margin: 1em 0;
border-collapse: collapse;
width: 100%;
overflow-x: auto;
display: block;
font-variant-numeric: lining-nums tabular-nums;
}
table caption {
margin-bottom: 0.75em;
}
tbody {
margin-top: 0.5em;
border-top: 1px solid #1a1a1a;
border-bottom: 1px solid #1a1a1a;
}
th {
border-top: 1px solid #1a1a1a;
padding: 0.25em 0.5em 0.25em 0.5em;
}
td {
padding: 0.125em 0.5em 0.25em 0.5em;
}
header {
margin-bottom: 4em;
text-align: center;
}
#TOC li {
list-style: none;
}
#TOC ul {
padding-left: 1.3em;
}
#TOC > ul {
padding-left: 0;
}
#TOC a:not(:hover) {
text-decoration: none;
}
code{white-space: pre-wrap;}
span.smallcaps{font-variant: small-caps;}
div.columns{display: flex; gap: min(4vw, 1.5em);}
div.column{flex: auto; overflow-x: auto;}
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
/* The extra [class] is a hack that increases specificity enough to
override a similar rule in reveal.js */
ul.task-list[class]{list-style: none;}
ul.task-list li input[type="checkbox"] {
font-size: inherit;
width: 0.8em;
margin: 0 0.8em 0.2em -1.6em;
vertical-align: middle;
}
.display.math{display: block; text-align: center; margin: 0.5rem auto;}
</style>
<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
<![endif]-->
</head>
<body>
<h1 id="chatscript-practicum-rejoinders">ChatScript Practicum:
Rejoinders</h1>
<p>Copyright Bruce Wilcox, mailto:gowilcox@gmail.com
www.brilligunderstanding.com <br>Revision 2/18/2018 cs8.1</p>
<p>’‘’There’s more than one way to skin a cat’’’. A problem often has
more than one solution. This is certainly true with ChatScript. The
purpose of the Practicum series is to show you how to think about
features of ChatScript and what guidelines to follow in designing and
coding your bot.</p>
<p>Responders and rejoinders are the workhorses of an
intelligent-appearing bot. They react to what the user says. Making them
work requires skill in writing rule patterns and skill in designing a
control script. Clever patterns are needed to detect the user’s intent.
A clever control script is needed to handle cases where users input more
than one sentence in a volley. But we’re not going to talk about either
of those important areas in this practicum.</p>
<p>We’re going to talk about rejoinders and how they can be modified or
enhanced with other engine functions.</p>
<h1 id="simple-rejoinders">Simple Rejoinders</h1>
<p>Of responders and rejoinders, rejoinders are the best and easiest.
Though they can follow any rule, generally they follow a gambit and
react on the user’s immediately following input. A well-crafted gambit
allows you a strong ability to predict what the user will say and
therefore the rejoinder pattern can often be trivial.</p>
<p>Simple bots try to tightly constrain the user by asking a yes-no
question, for which ChatScript has useful concept sets.</p>
<pre><code>t: Do you like pineapple?
a: (~yes) Yup, it's sweet and yummy
a: (~no) Too acidic for you, I guess.
a: (~dunno) How can you not know? </code></pre>
<p>Note that I indent rejoinders. This creates an easy visual structure
for understanding what is happening in script.</p>
<p>But yes-no questions get boring and obvious. The next level of
rejoinders works with a constrained set of possibilities. For
example:</p>
<pre><code>t: What natural disaster do you find most interesting?
a: (earthquake) It is exciting to feel everything moving.
a: (flood) Best to have a boat on standby.
a: (fire) They get a lot of fires in California.</code></pre>
<p>While these rejoinders would never work safely as a responder because
the pattern is too prone to false matches, given the context they are
very safe.</p>
<p>Note also that when a rejoinder matches, it completes the rule and no
other rejoinders will fire, whether or not any output is generated. This
may mean you want to order your rejoinders in a particular way to get
the best result, just as you would with any rules in a topic.</p>
<h1 id="refine">^refine()</h1>
<p>You can change the behavior of a rejoinder block by putting ^refine
on the main rule. This changes things so that instead of waiting for the
user’s next input, they act immediately on the current input. There are
a lot of useful characteristics from this. First, they can allow you to
speed up processing and make debugging easier by reducing tracing or
stepping. Consider the following sequence of gambits where the user has
clicked on some Google keyword about disasters and that data has been
passed into a bot:</p>
<pre><code>t: (!$started $keyword==earthquakes) Hi. You like earthquakes? $started = 1
t: (!$started $keyword==floods) Hi. You like floods? $started = 1
t: (!$started $keyword==fires) Hi. You like fires? $started = 1
t: (!$started) Hi. What disaster are you interested in? $started = 1</code></pre>
<p>As given above, each rule will be checked to see if it matches. When
one matches that rule erases. But all the other rules will try to fire
on any future input (failing because $started matches). If you’ve got a
lot of these, the system will waste time trying to match the rule (which
generally won’t matter because CS is fast). But if you are tracing or
stepping thru your code, you get a lot of noise. This is the perfect use
of a ^refine():</p>
<pre><code>t: () ^refine()
a: ($keyword==earthquakes) Hi. What do you want to know about earthquakes?
a: ($keyword==floods) Hi. What do you want to know about floods?
a: ($keyword==fires) Hi. What do you want to know about fires?
a: () Hi. What kind of disaster are you interested in? </code></pre>
<p>This code is smaller and faster. Rejoinders are not rules that are
separately erased. They automatically disappear as the rule above it
erases. So when the gambit fires, it will call on some rejoinder and
erase itself. We don’t need $started to block the unused choices. The
gambit, once erased, will not clog up tracing or stepping.</p>
<p>[Actually the engine never erases the code of a rule. It is merely
marked as disabled for that user and its code can even be reused by
other rules]</p>
<p>Similarly, you can do common subexpression on patterns if you want.
Imagine you have a hundred responders built to answer the question ‘who
is xxx’ where xxx is a famous person. The efficient way to write that
is:</p>
<pre><code>?: ( < who be) ^keep() ^refine()
a: (Mary Poppins) A babysitter
a: (Mickey Mouse) A Disney icon
...</code></pre>
<p>Here we want to be able to ask the same question about different
persons, so we add ^keep() to keep the top rule from erasing or we add
this to the topic flags so no responders in the topic erase
themselves.</p>
<p>But the ultimate in gambits with the user will involve totally
free-form questions. They are trickier to script and typically hunt for
keywords and affect words.</p>
<pre><code>t: Why are you a vegetarian?
a: ([ethical ethics moral principle]) That is very noble. But I like beef.
a: ([treatment pain]) Our current animal practices are not ideal.
a: (environment) Cows are environmentally good, when grazed properly.
a: (~badness) I understand your complaint, I just don't agree.
a: (~goodness) That benefit is generally overrated.</code></pre>
<h1 id="nested-rejoinders">Nested rejoinders</h1>
<p>One is tempted to put rejoinders on rejoinders. And if you could
realistically predict the user at each step you would have a great
conversation. But I’ve found it rarely pays to have a b: level rejoinder
(unless you asked a yes-no question on the a: level.</p>
<h1 id="finding-entitiesretryrule">Finding entities…^retry(RULE)</h1>
<p>When you write a bot, you probably need to both detect the intent
(what the user is inquiring about) and entities, which are specific
relevant information. For example for a weather bot, you need to detect
that the user wants a weather report, but you also probably need to
detect the location and the time involved.</p>
<p>Now imagine you are writing a medical bot, which needs to detect all
instances of blood (an entity) in a sentence. You can ask a rule to
repeat itself by using ^retry(RULE). The clever bit is that once a rule
matches, it remembers where the match starts, and a retry request will
execute the rule starting the pattern match after the prior match’s
start word.</p>
<pre><code>u: (_blood) ...do something with the match ... ^retry(RULE)</code></pre>
<p>If the input is ‘My blood is redder than your blood’, this rule will
first find blood at word 2, then restart the rule at word 3 and detect
the next blood at word 7.</p>
<p>But some references to blood may not actually about blood itself.
This use of refine combines sophisticated pattern matching with a
^refine block to only deal with the valid instances.</p>
<pre><code>u: (_blood) ^refine() ^retry(RULE)
a: (@_0+ pressure)
a: (@_0- red @_0+cell)
a: () ...do something with the match ...</code></pre>
<p>The fancy <span class="citation" data-cites="_0">@_0</span>+ and _0-
say: ’go to where the match _0 happened, and start matching either
backwards (<span class="citation" data-cites="_0">@_0</span>-) and then
forwards((<span class="citation" data-cites="_0">@_0</span>+). So the
first a: pattern jumps to word 2 and starts matching forward at word 3,
detecting ‘blood pressure’. The rejoinder fires and does nothing,
because we don’t care about this reference. Similarly the second
rejoinder hunts first immediately backward and then immediately
forwards, looking for ‘red blood cell’ and again not being interested.
The third rule accepts all other blood references and handles the data.
THEN, note that when ^refine completes, control returns to the top rule
and it then executes ^retry(RULE) to hunt for the next match. When,
eventually, no matches are found, the top rule fails and control moves
on. So here, the ^refine acts as a filter to decide which references are
important and which are not.</p>
<p>Notice that I put the ^retry(RULE) on the top rule. If I wanted some
rejoinders to retry the main rule and others not, I would put the ^retry
on the rejoinder itself and say ^retry(TOPRULE). Or I can put
^retry(RULE) on a rejoinder and have it try itself multiple times
locally and then to retry the main rule when it returns.</p>
<p>Also note that any rule whose pattern begins with a fixed location
element, like < or <span class="citation" data-cites="_0">@_0</span>+
cannot be successfully retried because the start location cannot be
moved past the match. The pattern explicitly says start here.</p>
<h1 id="shared-rejoinders---setrejoinder">Shared rejoinders -
^setrejoinder()</h1>
<p>Sometimes you want to share rejoinders across multiple rules. You can
do that easily enough by placing the rejoinders under one of the rules,
and having all other rules use ^setrejoinder and naming that rule.</p>
<pre><code>u: RULE1 (...)
a: (...)
a: (...)
u: RULE2 (...) ^setrejoinder(OUTPUT RULE1)</code></pre>
<p>RULE2 simply shares RULE1’s rejoinders. It doesn’t even matter if
RULE1 has been erased already. The rejoinders are still available.</p>
<p>Correspondingly there is a trick to create a common dummy area to be
reused…</p>
<pre><code>u: RULE1 (...) ^setrejoinder(OUTPUT RULE3)
u: RULE2 (...) ^setrejoinder(OUTPUT RULE3)
s: RULE3(?)
a: (...)
a: (...)
</code></pre>
<p>Here, RULE3 holds the common rejoinders, but itself can never
possibly fire because it is a contradiction. It will only be considered
for firing if the input is a statement (s:) but the pattern requires it
be a question.</p>
<h1 id="sequence">^sequence()</h1>
<p>Yet another function can modify the effects of a responder block.
^sequence is similar to ^refine, in that it matches on the current user
input rather than the next. But ^sequence differs in that it will try
every single rejoinder. Those that fire do their thing and the system
will keep testing them until all are tried.</p>
<pre><code>u: (I *~2 like _*1) ^sequence() ^retry(RULE)
a: (_0?~fruit) ... note fact about fruit ...
a: (_0?~yellowitems) ... note fact about yellow items...
a: (_0==banana) I love bananas.
a: (_0==grapefruit) I love grapefruit</code></pre>
<p>In the above we are detecting the user likes something in sample
sentence <code>I like bananas</code>. We want to record information and
sometimes react to it. So by executing in sequence every rejoinder, we
can capture that they like fruit, they like something yellow, and we can
react and comment about bananas. Grapefruit is not detected. And because
the user might say <code>I like bananas but I also like peaches</code>,
we can capture data about all this. If we only care about one occurrence
in the sentence then we just omit ^retry(RULE).</p>
<h1 id="rejoinder-substitutes---incontext">Rejoinder substitutes -
^incontext()</h1>
<p>The thing about simple rejoinders is that you can only match one of
them. But suppose you can anticipate the user asking more than one
question about your gambit.</p>
<pre><code>t: I lived in SF.
a: (when) 2 years ago.
a: (where) In the Presidio.</code></pre>
<p>In the above we have two likely questions a user might ask. But we
can only answer one of them. The other will fall to a responder. How do
we manage this? We use ^incontext().</p>
<pre><code>t: LIVEDSF() I lived in SF.
a: WHENSF (when) 2 years ago.
a: WHERESF (where) In the Presidio.
u: (when ^incontext(LIVEDSF) ^reuse(WHENSF)
u: (where ^incontext(LIVEDSF) ^reuse(WHERESF)</code></pre>
<p>^incontext returns true when a rule by the name supplied has executed
within the last 4 volleys. We could even skip the rejoinder code and put
the answers on the responders like this:</p>
<pre><code>t: LIVEDSF() I lived in SF.
u: (when ^incontext(LIVEDSF) 2 years ago.
u: (where ^incontext(LIVEDSF) ^ In the Presidio </code></pre>
<p>Note that you can have any number of rules with the same label. This
means we might even extend the life of the ^incontext by naming each the
same.</p>
<pre><code>t: LIVEDSF() I lived in SF.
u: LIVEDSF (when ^incontext(LIVEDSF) 2 years ago.
u: LIVEDSF (where ^incontext(LIVEDSF) ^ In the Presidio </code></pre>
<p>So here, if we execute the gambit, the user can ask us when and we
can catch that. And maybe they spend a couple of volleys talking about
the time 2 years ago. But they can still return to saying and where and
we still have context to reply about the Presidio.</p>
<h1 id="more-and-nextinput">%more and ^next(INPUT)</h1>
<p>Sometimes you want to try to use a rejoinder area to cover multiple
sentence input. Suppose your gambit asks ‘do you like fruit’. Someone
might answer ‘yes’, someone else might answer ‘I like bananas’, and
someone else might answer ‘yes. I like bananas’. And you start to write
a rejoinder to detect those cases maybe like this:</p>
<pre><code>t: Do you like fruit?
a: (~yes !%more) What kind of fruit?
a: (banana) I love bananas.</code></pre>
<p>Your first rejoinder detects <code>yes</code>. And using %more can
tell that there is no more input from the user in this volley, so it’s
safe to follow up with what kind of fruit. Your second rejoinder detects
the case where they simply reply <code>I like bananas</code>. But what
do you do for the third case which combines the two?</p>
<p>You can use ^next(input) to slide along to their second input, and
then try to match that against the rejoinders. I.e.,</p>
<pre><code>t: FRUIT() Do you like fruit?
a: (~yes !%more) What kind of fruit?
a: (banana) I love bananas.
a: (%more) ^next(INPUT) ^refine(FRUIT)</code></pre>
<p>The third rejoinder says since there is more input and we already
have handled anything the chatbot could want about the yes input, change
the system at this moment to be using the next input. So suddenly
everything is set up to process rules on <code>I like bananas</code>.
And then we simple tell the system to take that current input and run
^refine() on the block of rejoinders. So it can detect banana and
respond. And if it can’t detect anything, you just drop out of the
rejoinders and continue with normal script execution.</p>
<p>Actually, that third rule is faulty. Can you figure out why?</p>
<p>If there are 3 inputs, the first of which is yes and the second of
which doesnt match <code>bananas</code>, then it will execute again,
lose the second input and move on to the third. That seems bad. You can
easily add a flag into the pattern and the output so that it will only
execute once, regardless.</p>
<h1 id="rejoinders-and-control-scripts">Rejoinders and Control
Scripts</h1>
<p>A control script typically tests for out-of-band inputs before
anything else. They are treated by CS as a separate sentence in the
user’s input and the control script will process it and ^end(SENTENCE).
So it never gets to ^rejoinder() processing for that. But normal user
sentences will typically go to rejoinders before trying to do responders
or gambits anywhere. So what does it mean if your rejoinder fails to
fire?</p>
<p>It could mean, of course that your rule pattern didn’t match. Or it
could mean you messed up the control script before the rejoinder call
(assuming you changed the control script). One way to fail is to have a
bug such that the script fails and so ends prematurely. You can protect
against some of that using ^nofail(RULE). Another way to fail is to
actually generate user output from earlier in your control script. After
all, CS normally stops when it gets some user output.</p>
<p>The other thing to note is that the engine api ^rejoinder() is not
something sacred. One can write script to perform custom behaviors that
either use it or replace it. Remember this example?</p>
<pre><code>t: FRUIT() Do you like fruit?
a: (~yes !%more) What kind of fruit?
a: (banana) I love bananas.
a: (%more) ^next(INPUT) ^refine(FRUIT)</code></pre>
<p>While it’s clever, one has to write that %more rule on every
rejoinder block you want it to happen. Makes for ugly code. So in my
chatbot I have custom rejoinder script that does that automatically. It
allows me to write simpler cleaner code like this:</p>
<pre><code>t: FRUIT() Do you like fruit?
a: (~yes) What kind of fruit?
a: (banana) I love bananas.</code></pre>
<p>It gets the rule tag for where the rejoinder is, it walks the rules
of the rejoinder block and retrieves their pattern code. If the user
input is yes, it sees if the block has a ~yes handler and if it does,
performs the ^next and then tests the result against all the rejoinder
choices. If there’s a match it performs the output of that rule. All in
script, never calling the system’s ^rejoinder() code. I leave it to your
research to find the appropriate engine calls for this.</p>
<h1 id="summary">Summary</h1>
<p>As a general rule, reducing rule count to improve speed is a waste of
time (ChatScript is too fast). But moving rules into rejoinder areas
attached to a rule pays off in debugging time (tracing and stepping) and
often makes the code intent clearer.</p>
<p>And there are all these other interesting uses that rejoinders can be
put to.</p>
</body>
</html>
You can’t perform that action at this time.
