markov_chain_chat_bot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README ADDED
@@ -0,0 +1,44 @@
1
+ == Markov chain chat bot
2
+
3
+
4
+ A chat bot utilizing Markov chains. It speaks Russian and English.
5
+
6
+
7
+
8
+
9
+
10
+ === Examples
11
+
12
+
13
+ Basic usage:
14
+
15
+ require 'chat_bot'
16
+
17
+ bot = ChatBot.from(Hash.new)
18
+ bot.learn("one two three two one")
19
+ bot.answer("count up and down please")
20
+ #=> "one two three two three two one two one two three two one two one"
21
+ bot.learn("three four six")
22
+ bot.answer("count from three please")
23
+ #=> "three two one two one two three four six"
24
+
25
+ One may save the bot's knowledge into key-value storage:
26
+
27
+ require 'chat_bot'
28
+ require 'auto_marshalling_map'
29
+ require 'gdbm'
30
+
31
+ # 1.
32
+ kvs = GDBM.open("chat_bot.dat")
33
+ bot = ChatBot.from(AutoMarhsallingMap.new(kvs))
34
+ bot.learn("one two three two one")
35
+ kvs.close()
36
+
37
+ # 2.
38
+ kvs = GDBM.open("chat_bot.dat")
39
+ bot = ChatBot.from(AutoMarhsallingMap.new(kvs))
40
+ bot.answer("count up and down please")
41
+ #=> "one two three two three two three two one two one"
42
+
43
+
44
+
@@ -0,0 +1,29 @@
1
+
2
+ #
3
+ # A map which automatically marshals all keys and values passed to it.
4
+ #
5
+ class AutoMarshallingMap
6
+
7
+ #
8
+ # +backend+ is a Map from String to (String or nil). It will be used as
9
+ # a backend storage for the new AutoMarshallingMap.
10
+ #
11
+ def initialize(backend)
12
+ @backend = backend
13
+ end
14
+
15
+ def [](key)
16
+ value_string = @backend[Marshal.dump(key)]
17
+ return nil if value_string.nil?
18
+ return Marshal.load value_string
19
+ end
20
+
21
+ def []=(key, value)
22
+ @backend[Marshal.dump(key)] = Marshal.dump(value)
23
+ return value
24
+ end
25
+
26
+ # +backend+ argument passed to #new().
27
+ attr_reader :backend
28
+
29
+ end
data/lib/chat_bot.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'markov_chain_chat_bot'
2
+
3
+ #
4
+ # Deprecated. Use MarkovChainChatBot instead.
5
+ #
6
+ ChatBot = MarkovChainChatBot
@@ -0,0 +1,93 @@
1
+
2
+ class MarkovChain
3
+
4
+ #
5
+ # +data+ is #data() of the other MarkovChain. It becomes owned by the
6
+ # returned MarkovChain.
7
+ #
8
+ def self.from(data)
9
+ new(data)
10
+ end
11
+
12
+ #
13
+ # creates an empty MarkovChain.
14
+ #
15
+ # +data+ is a map which becomes owned by this MarkovChain.
16
+ #
17
+ def initialize(data = {})
18
+ @data = data
19
+ @last_state = nil
20
+ end
21
+
22
+ #
23
+ # appends +states+ to the end of this MarkovChain.
24
+ #
25
+ # +states+ are arbitrary objects.
26
+ #
27
+ # It returns this (modified) MarkovChain.
28
+ #
29
+ def append!(states)
30
+ for next_state in states
31
+ state_occurences_map = (@data[@last_state] or Hash.new)
32
+ state_occurences_map[next_state] ||= 0
33
+ state_occurences_map[next_state] += 1
34
+ @data[@last_state] = state_occurences_map
35
+ @last_state = next_state
36
+ end
37
+ return self
38
+ end
39
+
40
+ #
41
+ # returns Enumerable of predicted states. The result may contain nils if
42
+ # the MarkovChain can not predict a state.
43
+ #
44
+ def predict()
45
+ self.extend(Prediction)
46
+ end
47
+
48
+ #
49
+ # +data+ passed to #initialize().
50
+ #
51
+ def data
52
+ @data
53
+ end
54
+
55
+ private
56
+
57
+ # :enddoc:
58
+
59
+ #
60
+ # This module is only intended for inclusion into MarkovChain.
61
+ #
62
+ module Prediction
63
+
64
+ include Enumerable
65
+
66
+ def each
67
+ #
68
+ last_state = @last_state
69
+ loop do
70
+ #
71
+ next_state = begin
72
+ state_occurences_map = (@data[last_state] or Hash.new)
73
+ occurences_sum = state_occurences_map.reduce(0) do |sum, entry|
74
+ sum + entry[1]
75
+ end
76
+ choice = rand(occurences_sum + 1)
77
+ chosen_state_and_occurences = state_occurences_map.find do |state, occurences|
78
+ choice -= occurences
79
+ choice <= 0
80
+ end
81
+ chosen_state_and_occurences ||= [nil, nil]
82
+ chosen_state_and_occurences[0]
83
+ end
84
+ #
85
+ yield next_state
86
+ #
87
+ last_state = next_state
88
+ end
89
+ end
90
+
91
+ end
92
+
93
+ end
@@ -0,0 +1,560 @@
1
+ # encoding: UTF-8
2
+ require 'markov_chain'
3
+ require 'stringio'
4
+
5
+ #
6
+ # A chat bot utilizing MarkovChain.
7
+ #
8
+ class MarkovChainChatBot
9
+
10
+ private_class_method :new
11
+
12
+ #
13
+ # +data+ is a map. It may be empty, in this case a brand new ChatBot is
14
+ # created. +data+ becomes owned by the returned ChatBot.
15
+ #
16
+ # +answer_limit+ is maximum size of the result of #answer().
17
+ #
18
+ def self.from(data, answer_limit = 1000)
19
+ new(data, answer_limit)
20
+ end
21
+
22
+ def initialize(data, answer_limit) # :nodoc:
23
+ @markov_chain =
24
+ if data.empty? then MarkovChain.new(data)
25
+ else MarkovChain.from(data)
26
+ end
27
+ @answer_limit = answer_limit
28
+ end
29
+
30
+ # +data+ passed to #from().
31
+ def data
32
+ @markov_chain.data
33
+ end
34
+
35
+ def learn(text)
36
+ @markov_chain.append!(tokenize(text)).append!([EndOfMessage.new])
37
+ end
38
+
39
+ def answer(question)
40
+ answer = ""
41
+ previous_token = nil
42
+ catch :out_of_limit do
43
+ for token in @markov_chain.predict()
44
+ break if token.tkn_is_a? EndOfMessage or token.nil?
45
+ delimiter =
46
+ if (previous_token.tkn_is_a? Word and token.tkn_is_a? Word) then " "
47
+ else ""
48
+ end
49
+ answer.append_limited(delimiter + token.tkn_value, @answer_limit)
50
+ previous_token = token
51
+ end
52
+ end
53
+ return answer
54
+ end
55
+
56
+ private
57
+
58
+ # :enddoc:
59
+
60
+ class ::String
61
+
62
+ # appends +appendment+ to this String or throws +:out_of_limit+ if
63
+ # this String will exceed +limit+ after the appending.
64
+ #
65
+ # It returns this (modified) String.
66
+ #
67
+ def append_limited(appendment, limit)
68
+ throw :out_of_limit if self.length + appendment.length > limit
69
+ self << appendment
70
+ return self
71
+ end
72
+
73
+ end
74
+
75
+ Token = Object
76
+
77
+ class Token
78
+
79
+ def tkn_value
80
+ self[1..-1]
81
+ end
82
+
83
+ def tkn_is_a?(clazz)
84
+ clazz === self
85
+ end
86
+
87
+ end
88
+
89
+ class Word < Token
90
+
91
+ def self.new(value)
92
+ "w" + value
93
+ end
94
+
95
+ def self.===(x)
96
+ x.is_a? String and x[0] == "w"
97
+ end
98
+
99
+ end
100
+
101
+ class PunctuationMark < Token
102
+
103
+ def self.new(value)
104
+ "p" + value
105
+ end
106
+
107
+ def self.===(x)
108
+ x.is_a? String and x[0] == "p"
109
+ end
110
+
111
+ end
112
+
113
+ class EndOfMessage < Token
114
+
115
+ def self.new()
116
+ nil
117
+ end
118
+
119
+ def self.===(x)
120
+ x.nil?
121
+ end
122
+
123
+ end
124
+
125
+ #
126
+ # returns Array of Token-s.
127
+ #
128
+ def tokenize(text)
129
+ yy_parse(StringIO.new(text))
130
+ end
131
+
132
+
133
+
134
+ #
135
+ # +input+ is IO. It must have working IO#pos, IO#pos= and
136
+ # IO#set_encoding() methods.
137
+ #
138
+ # It may raise YY_SyntaxError.
139
+ #
140
+ def yy_parse(input)
141
+ input.set_encoding("UTF-8", "UTF-8")
142
+ context = YY_ParsingContext.new(input)
143
+ yy_from_pcv(
144
+ yy_nonterm1(context) ||
145
+ # TODO: context.worst_error can not be nil here. Prove it.
146
+ raise(context.worst_error)
147
+ )
148
+ end
149
+
150
+ # TODO: Allow to pass String to the entry point.
151
+
152
+
153
+ # :nodoc:
154
+ ### converts value to parser-compatible value (which is always non-false and
155
+ ### non-nil).
156
+ def yy_to_pcv(value)
157
+ if value.nil? then :yy_nil
158
+ elsif value == false then :yy_false
159
+ else value
160
+ end
161
+ end
162
+
163
+ # :nodoc:
164
+ ### converts value got by #yy_to_pcv() to actual value.
165
+ def yy_from_pcv(value)
166
+ if value == :yy_nil then nil
167
+ elsif value == :yy_false then false
168
+ else value
169
+ end
170
+ end
171
+
172
+ # :nodoc:
173
+ class YY_ParsingContext
174
+
175
+ # +input+ is IO.
176
+ def initialize(input)
177
+ @input = input
178
+ @worst_error = nil
179
+ end
180
+
181
+ attr_reader :input
182
+
183
+ # It is YY_SyntaxExpectationError or nil.
184
+ attr_accessor :worst_error
185
+
186
+ # adds possible error to this YY_ParsingContext.
187
+ #
188
+ # +error+ is YY_SyntaxExpectationError.
189
+ #
190
+ def << error
191
+ # Update worst_error.
192
+ if worst_error.nil? or worst_error.pos < error.pos then
193
+ @worst_error = error
194
+ elsif worst_error.pos == error.pos then
195
+ @worst_error = @worst_error.or error
196
+ end
197
+ #
198
+ return self
199
+ end
200
+
201
+ end
202
+
203
+ # :nodoc:
204
+ def yy_string(context, string)
205
+ #
206
+ string_start_pos = context.input.pos
207
+ # Read string.
208
+ read_string = context.input.read(string.bytesize)
209
+ # Set the string's encoding; check if it fits the argument.
210
+ unless read_string and (read_string.force_encoding(Encoding::UTF_8)) == string then
211
+ #
212
+ context << YY_SyntaxExpectationError.new(yy_displayed(string), string_start_pos)
213
+ #
214
+ return nil
215
+ end
216
+ #
217
+ return read_string
218
+ end
219
+
220
+ # :nodoc:
221
+ def yy_end?(context)
222
+ #
223
+ if not context.input.eof?
224
+ context << YY_SyntaxExpectationError.new("the end", context.input.pos)
225
+ return nil
226
+ end
227
+ #
228
+ return true
229
+ end
230
+
231
+ # :nodoc:
232
+ def yy_begin?(context)
233
+ #
234
+ if not (context.input.pos == 0)
235
+ context << YY_SyntaxExpectationError.new("the beginning", context.input.pos)
236
+ return nil
237
+ end
238
+ #
239
+ return true
240
+ end
241
+
242
+ # :nodoc:
243
+ def yy_char(context)
244
+ #
245
+ char_start_pos = context.input.pos
246
+ # Read a char.
247
+ c = context.input.getc
248
+ #
249
+ unless c then
250
+ #
251
+ context << YY_SyntaxExpectationError.new("a character", char_start_pos)
252
+ #
253
+ return nil
254
+ end
255
+ #
256
+ return c
257
+ end
258
+
259
+ # :nodoc:
260
+ def yy_char_range(context, from, to)
261
+ #
262
+ char_start_pos = context.input.pos
263
+ # Read the char.
264
+ c = context.input.getc
265
+ # Check if it fits the range.
266
+ # NOTE: c has UTF-8 encoding.
267
+ unless c and (from <= c and c <= to) then
268
+ #
269
+ context << YY_SyntaxExpectationError.new(%(#{yy_displayed from}...#{yy_displayed to}), char_start_pos)
270
+ #
271
+ return nil
272
+ end
273
+ #
274
+ return c
275
+ end
276
+
277
+ # :nodoc:
278
+ ### The form of +string+ suitable for displaying in messages.
279
+ def yy_displayed(string)
280
+ if string.length == 1 then
281
+ char = string[0]
282
+ char_code = char.ord
283
+ case char_code
284
+ when 0x00...0x20, 0x2028, 0x2029 then %(#{yy_unicode_s char_code})
285
+ when 0x20...0x80 then %("#{char}")
286
+ when 0x80...Float::INFINITY then %("#{char} (#{yy_unicode_s char_code})")
287
+ end
288
+ else
289
+ %("#{string}")
290
+ end
291
+ end
292
+
293
+ # :nodoc:
294
+ ### "U+XXXX" string corresponding to +char_code+.
295
+ def yy_unicode_s(char_code)
296
+ "U+#{"%04X" % char_code}"
297
+ end
298
+
299
+ class YY_SyntaxError < Exception
300
+
301
+ def initialize(message, pos)
302
+ super(message)
303
+ @pos = pos
304
+ end
305
+
306
+ attr_reader :pos
307
+
308
+ end
309
+
310
+ # :nodoc:
311
+ class YY_SyntaxExpectationError < YY_SyntaxError
312
+
313
+ #
314
+ # +expectations+ are String-s.
315
+ #
316
+ def initialize(*expectations, pos)
317
+ super(nil, pos)
318
+ @expectations = expectations
319
+ end
320
+
321
+ #
322
+ # returns other YY_SyntaxExpectationError with #expectations combined.
323
+ #
324
+ # +other+ is another YY_SyntaxExpectationError.
325
+ #
326
+ # #pos of this YY_SyntaxExpectationError and +other+ must be equal.
327
+ #
328
+ def or other
329
+ raise %(can not "or" #{YY_SyntaxExpectationError}s with different pos) unless self.pos == other.pos
330
+ YY_SyntaxExpectationError.new(*(self.expectations + other.expectations), pos)
331
+ end
332
+
333
+ def message
334
+ expectations = self.expectations.uniq
335
+ [expectations[0...-1].join(", "), expectations[-1]].join(" or ") + " is expected"
336
+ end
337
+
338
+ protected
339
+
340
+ # Private
341
+ attr_reader :expectations
342
+
343
+ end
344
+
345
+ # :nodoc:
346
+ def yy_nonterm1(yy_context)
347
+ val = :yy_nil
348
+ (begin
349
+ val = []
350
+ true
351
+ end and while true
352
+ yy_vare = yy_context.input.pos
353
+ if not begin; yy_var9 = yy_context.input.pos; (begin
354
+ yy_vara = yy_nontermf(yy_context)
355
+ if yy_vara then
356
+ w = yy_from_pcv(yy_vara)
357
+ end
358
+ yy_vara
359
+ end and begin
360
+ val << Word.new(w)
361
+ true
362
+ end) or (yy_context.input.pos = yy_var9; (begin
363
+ yy_varb = yy_nonterm14(yy_context)
364
+ if yy_varb then
365
+ p = yy_from_pcv(yy_varb)
366
+ end
367
+ yy_varb
368
+ end and begin
369
+ val << PunctuationMark.new(p)
370
+ true
371
+ end)) or (yy_context.input.pos = yy_var9; yy_nonterm20(yy_context) and while true
372
+ yy_vard = yy_context.input.pos
373
+ if not yy_nonterm20(yy_context) then
374
+ yy_context.input.pos = yy_vard
375
+ break true
376
+ end
377
+ end); end then
378
+ yy_context.input.pos = yy_vare
379
+ break true
380
+ end
381
+ end) and yy_to_pcv(val)
382
+ end
383
+
384
+ # :nodoc:
385
+ def yy_nontermf(yy_context)
386
+ val = :yy_nil
387
+ (begin
388
+ val = ""
389
+ true
390
+ end and begin; yy_varp = yy_context.input.pos; begin
391
+ yy_varq = yy_nontermx(yy_context)
392
+ if yy_varq then
393
+ val << yy_from_pcv(yy_varq)
394
+ end
395
+ yy_varq
396
+ end or (yy_context.input.pos = yy_varp; (begin
397
+ yy_varr = yy_string(yy_context, "-")
398
+ if yy_varr then
399
+ h = yy_from_pcv(yy_varr)
400
+ end
401
+ yy_varr
402
+ end and begin
403
+ yy_varu = yy_context.input.pos
404
+ yy_varv = yy_nontermx(yy_context)
405
+ yy_context.input.pos = yy_varu
406
+ yy_varv
407
+ end and begin
408
+ val << h
409
+ true
410
+ end)); end and while true
411
+ yy_varw = yy_context.input.pos
412
+ if not begin; yy_varp = yy_context.input.pos; begin
413
+ yy_varq = yy_nontermx(yy_context)
414
+ if yy_varq then
415
+ val << yy_from_pcv(yy_varq)
416
+ end
417
+ yy_varq
418
+ end or (yy_context.input.pos = yy_varp; (begin
419
+ yy_varr = yy_string(yy_context, "-")
420
+ if yy_varr then
421
+ h = yy_from_pcv(yy_varr)
422
+ end
423
+ yy_varr
424
+ end and begin
425
+ yy_varu = yy_context.input.pos
426
+ yy_varv = yy_nontermx(yy_context)
427
+ yy_context.input.pos = yy_varu
428
+ yy_varv
429
+ end and begin
430
+ val << h
431
+ true
432
+ end)); end then
433
+ yy_context.input.pos = yy_varw
434
+ break true
435
+ end
436
+ end) and yy_to_pcv(val)
437
+ end
438
+
439
+ # :nodoc:
440
+ def yy_nontermx(yy_context)
441
+ val = :yy_nil
442
+ begin; yy_vary = yy_context.input.pos; begin
443
+ yy_varz = yy_char_range(yy_context, "a", "z")
444
+ if yy_varz then
445
+ val = yy_from_pcv(yy_varz)
446
+ end
447
+ yy_varz
448
+ end or (yy_context.input.pos = yy_vary; begin
449
+ yy_var10 = yy_char_range(yy_context, "A", "Z")
450
+ if yy_var10 then
451
+ val = yy_from_pcv(yy_var10)
452
+ end
453
+ yy_var10
454
+ end) or (yy_context.input.pos = yy_vary; begin
455
+ yy_var11 = yy_char_range(yy_context, "\u{430}", "\u{44f}")
456
+ if yy_var11 then
457
+ val = yy_from_pcv(yy_var11)
458
+ end
459
+ yy_var11
460
+ end) or (yy_context.input.pos = yy_vary; begin
461
+ yy_var12 = yy_char_range(yy_context, "\u{410}", "\u{42f}")
462
+ if yy_var12 then
463
+ val = yy_from_pcv(yy_var12)
464
+ end
465
+ yy_var12
466
+ end) or (yy_context.input.pos = yy_vary; begin
467
+ yy_var13 = yy_char_range(yy_context, "0", "9")
468
+ if yy_var13 then
469
+ val = yy_from_pcv(yy_var13)
470
+ end
471
+ yy_var13
472
+ end); end and yy_to_pcv(val)
473
+ end
474
+
475
+ # :nodoc:
476
+ def yy_nonterm14(yy_context)
477
+ val = :yy_nil
478
+ (begin
479
+ val = ""
480
+ true
481
+ end and while true
482
+ yy_var1b = yy_context.input.pos
483
+ if not begin
484
+ yy_var1a = yy_nonterm20(yy_context)
485
+ if yy_var1a then
486
+ val << yy_from_pcv(yy_var1a)
487
+ end
488
+ yy_var1a
489
+ end then
490
+ yy_context.input.pos = yy_var1b
491
+ break true
492
+ end
493
+ end and (begin
494
+ yy_var1p = yy_context.worst_error
495
+ begin
496
+ not begin
497
+ yy_var1q = yy_context.input.pos
498
+ yy_var1r = yy_nontermx(yy_context)
499
+ yy_context.input.pos = yy_var1q
500
+ yy_var1r
501
+ end
502
+ ensure
503
+ yy_context.worst_error = yy_var1p
504
+ end
505
+ end and begin
506
+ yy_var1s = yy_char(yy_context)
507
+ if yy_var1s then
508
+ val << yy_from_pcv(yy_var1s)
509
+ end
510
+ yy_var1s
511
+ end) and while true
512
+ yy_var1t = yy_context.input.pos
513
+ if not (begin
514
+ yy_var1p = yy_context.worst_error
515
+ begin
516
+ not begin
517
+ yy_var1q = yy_context.input.pos
518
+ yy_var1r = yy_nontermx(yy_context)
519
+ yy_context.input.pos = yy_var1q
520
+ yy_var1r
521
+ end
522
+ ensure
523
+ yy_context.worst_error = yy_var1p
524
+ end
525
+ end and begin
526
+ yy_var1s = yy_char(yy_context)
527
+ if yy_var1s then
528
+ val << yy_from_pcv(yy_var1s)
529
+ end
530
+ yy_var1s
531
+ end) then
532
+ yy_context.input.pos = yy_var1t
533
+ break true
534
+ end
535
+ end and while true
536
+ yy_var1z = yy_context.input.pos
537
+ if not begin
538
+ yy_var1y = yy_nonterm20(yy_context)
539
+ if yy_var1y then
540
+ val << yy_from_pcv(yy_var1y)
541
+ end
542
+ yy_var1y
543
+ end then
544
+ yy_context.input.pos = yy_var1z
545
+ break true
546
+ end
547
+ end) and yy_to_pcv(val)
548
+ end
549
+
550
+ # :nodoc:
551
+ def yy_nonterm20(yy_context)
552
+ val = :yy_nil
553
+ (begin; yy_var23 = yy_context.input.pos; yy_char_range(yy_context, "\t", "\r") or (yy_context.input.pos = yy_var23; yy_string(yy_context, " ")) or (yy_context.input.pos = yy_var23; yy_string(yy_context, "\u{85}")) or (yy_context.input.pos = yy_var23; yy_string(yy_context, "\u{a0}")) or (yy_context.input.pos = yy_var23; yy_string(yy_context, "\u{1680}")) or (yy_context.input.pos = yy_var23; yy_string(yy_context, "\u{180e}")) or (yy_context.input.pos = yy_var23; yy_char_range(yy_context, "\u{2000}", "\u{200a}")) or (yy_context.input.pos = yy_var23; yy_string(yy_context, "\u{2028}")) or (yy_context.input.pos = yy_var23; yy_string(yy_context, "\u{2029}")) or (yy_context.input.pos = yy_var23; yy_string(yy_context, "\u{202f}")) or (yy_context.input.pos = yy_var23; yy_string(yy_context, "\u{205f}")) or (yy_context.input.pos = yy_var23; yy_string(yy_context, "\u{3000}")); end and begin
554
+ val = " "
555
+ true
556
+ end) and yy_to_pcv(val)
557
+ end
558
+
559
+ end
560
+
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: markov_chain_chat_bot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Lavir the Whiolet
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-07-23 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: A chat bot utilizing Markov chains. It speaks Russian and English.
15
+ email: Lavir.th.Whiolet@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files:
19
+ - README
20
+ files:
21
+ - lib/auto_marshalling_map.rb
22
+ - lib/chat_bot.rb
23
+ - lib/markov_chain.rb
24
+ - lib/markov_chain_chat_bot.rb
25
+ - README
26
+ homepage: https://github.com/LavirtheWhiolet/markov-chain-bot-module
27
+ licenses:
28
+ - Public Domain
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ! '>='
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubyforge_project:
47
+ rubygems_version: 1.8.23
48
+ signing_key:
49
+ specification_version: 3
50
+ summary: Markov chain chat bot
51
+ test_files: []