rivescript 0.1.1

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/lib/rivescript.rb ADDED
@@ -0,0 +1,460 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RiveScript Ruby port
4
+ # Byte <byte@jvmlab.org>, https://jvmlab.org/
5
+ # MIT License
6
+
7
+ require_relative "rivescript/version"
8
+ require_relative "rivescript/utils"
9
+ require_relative "rivescript/sessions"
10
+ require_relative "rivescript/parser"
11
+ require_relative "rivescript/brain"
12
+ require_relative "rivescript/sorting"
13
+ require_relative "rivescript/inheritance"
14
+ require_relative "rivescript/lang/ruby"
15
+
16
+ # RiveScript interpreter for Ruby.
17
+ #
18
+ # Create a new instance with optional configuration:
19
+ #
20
+ # bot = RiveScript.new(debug: true, utf8: true)
21
+ # bot.load_directory("brain")
22
+ # bot.sort_replies
23
+ # reply = bot.reply("user", "hello")
24
+ class RiveScript
25
+ attr_accessor :unicode_punctuation, :errors
26
+ attr_reader :_strict, :_utf8, :_depth, :_force_case, :_concat, :_case_sensitive,
27
+ :_global, :_var, :_sub, :_submax, :_person, :_personmax, :_array,
28
+ :_session, :_includes, :_inherits, :_handlers, :_objlangs, :_topics,
29
+ :_thats, :_sorted, :parser, :brain
30
+
31
+ alias _forceCase _force_case
32
+
33
+ # Gem / checkout root (directory that contains lib/ and eg/).
34
+ def self.root
35
+ File.expand_path("..", __dir__)
36
+ end
37
+
38
+ # Path to the bundled sample brain shipped with the gem.
39
+ #
40
+ # bot = RiveScript.new
41
+ # bot.load_directory(RiveScript.brain_path)
42
+ # bot.sort_replies
43
+ def self.brain_path
44
+ File.join(root, "eg", "brain")
45
+ end
46
+
47
+ # @param opts [Hash] configuration options
48
+ # @option opts [Boolean] :debug debug mode (default false)
49
+ # @option opts [Integer] :depth recursion depth limit (default 50)
50
+ # @option opts [Boolean] :strict strict mode (default true)
51
+ # @option opts [Boolean] :utf8 enable UTF-8 mode (default false)
52
+ # @option opts [Boolean] :force_case force-lowercase triggers (default false)
53
+ # @option opts [Proc] :on_debug custom debug log handler
54
+ # @option opts [String] :concat global concatenation mode override
55
+ # @option opts [RiveScript::SessionManager] :session_manager custom session store
56
+ # @option opts [Boolean] :case_sensitive preserve user message capitalization
57
+ # @option opts [Boolean] :enable_object_macros register the default Ruby object
58
+ # macro handler (default true). Set to +false+ to disable executing Ruby
59
+ # object macros defined in RiveScript source.
60
+ # @option opts [Regexp] :unicode_punctuation punctuation regexp for UTF-8 mode
61
+ # @option opts [Hash] :errors customized error messages
62
+ def initialize(opts = {})
63
+ opts = {} if opts.nil?
64
+
65
+ @_debug = opts.fetch(:debug, false)
66
+ @_strict = opts.fetch(:strict, true)
67
+ @_depth = opts.key?(:depth) ? opts[:depth].to_i : 50
68
+ @_utf8 = opts.fetch(:utf8, false)
69
+ @_force_case = opts.fetch(:force_case, false)
70
+ @on_debug = opts[:on_debug]
71
+ @_concat = opts[:concat]
72
+ @_case_sensitive = opts.fetch(:case_sensitive, false)
73
+
74
+ @unicode_punctuation = opts.fetch(:unicode_punctuation, /[.,!?;:]/)
75
+
76
+ @errors = {
77
+ "replyNotMatched" => "ERR: No Reply Matched",
78
+ "replyNotFound" => "ERR: No Reply Found",
79
+ "objectNotFound" => "[ERR: Object Not Found]",
80
+ "deepRecursion" => "ERR: Deep Recursion Detected"
81
+ }
82
+ if opts[:errors].is_a?(Hash)
83
+ opts[:errors].each do |key, value|
84
+ @errors[key.to_s] = value
85
+ end
86
+ end
87
+
88
+ @runtime = runtime
89
+
90
+ @parser = Parser.new(self)
91
+ @brain = Brain.new(self)
92
+
93
+ @_pending = []
94
+ @_load_count = 0
95
+
96
+ @_global = {}
97
+ @_var = {}
98
+ @_sub = {}
99
+ @_submax = 1
100
+ @_person = {}
101
+ @_personmax = 1
102
+ @_array = {}
103
+ @_session = opts[:session_manager]
104
+ @_includes = {}
105
+ @_inherits = {}
106
+ @_handlers = {}
107
+ @_objlangs = {}
108
+ @_topics = {}
109
+ @_thats = {}
110
+ @_sorted = {}
111
+
112
+ @_session = MemorySessionManager.new if @_session.nil?
113
+
114
+ @_handlers["ruby"] = Lang::RubyHandler.new(self) unless opts[:enable_object_macros] == false
115
+ say("RiveScript Interpreter v#{VERSION} Initialized.")
116
+ say("Runtime Environment: #{@runtime}")
117
+ end
118
+
119
+ # Returns the version number of the RiveScript Ruby library.
120
+ def version
121
+ VERSION
122
+ end
123
+
124
+ # Load a RiveScript document from one or more files.
125
+ def load_file(path)
126
+ paths = path.is_a?(Array) ? path : [path]
127
+
128
+ paths.each do |file|
129
+ say("Request to load file: #{file}")
130
+ data = File.read(file)
131
+ ok = parse(file, data)
132
+ raise "parser error" unless ok
133
+ end
134
+
135
+ true
136
+ end
137
+
138
+ # Load RiveScript documents from a directory recursively.
139
+ def load_directory(path)
140
+ raise "#{path} is not a directory" unless File.directory?(path)
141
+
142
+ say("Loading from directory #{path}")
143
+ files = Dir.glob(File.join(path, "**", "*.{rive,rs}"), File::FNM_CASEFOLD)
144
+ load_file(files)
145
+ end
146
+
147
+ # Load RiveScript source code from a string.
148
+ # Returns true if the code parsed with no error.
149
+ def stream(code, on_error = nil)
150
+ parse("stream()", code, on_error)
151
+ end
152
+
153
+ # Parse RiveScript code and load it into memory.
154
+ def parse(filename, code, on_error = nil)
155
+ say("Parsing code!")
156
+
157
+ ok = true
158
+ error_handler = lambda do |err, fn, ln|
159
+ on_error&.call(err, fn, ln)
160
+ ok = false
161
+ end
162
+ ast = @parser.parse(filename, code, error_handler)
163
+
164
+ ast["begin"].each do |type, vars|
165
+ internal = :"@_#{type}"
166
+ max_key = :"@_#{type}max"
167
+
168
+ vars.each do |name, value|
169
+ if %w[sub person].include?(type)
170
+ instance_variable_set(max_key, [instance_variable_get(max_key), name.split(" ").length].max)
171
+ end
172
+
173
+ target = instance_variable_get(internal)
174
+ if value == "<undef>"
175
+ target.delete(name)
176
+ else
177
+ target[name] = value
178
+ end
179
+ end
180
+ end
181
+
182
+ @_debug = @_global["debug"] == "true" if @_global.key?("debug")
183
+ if @_global.key?("depth")
184
+ parsed_depth = @_global["depth"].to_i
185
+ @_depth = parsed_depth.zero? ? 50 : parsed_depth
186
+ end
187
+
188
+ ast["topics"].each do |topic, data|
189
+ @_includes[topic] ||= {}
190
+ @_inherits[topic] ||= {}
191
+ Utils.extend(@_includes[topic], data["includes"])
192
+ Utils.extend(@_inherits[topic], data["inherits"])
193
+
194
+ @_topics[topic] ||= []
195
+ data["triggers"].each do |trigger|
196
+ @_topics[topic] << trigger
197
+
198
+ next if trigger["previous"].nil?
199
+
200
+ @_thats[topic] ||= {}
201
+ @_thats[topic][trigger["trigger"]] ||= {}
202
+ @_thats[topic][trigger["trigger"]][trigger["previous"]] = trigger
203
+ end
204
+ end
205
+
206
+ ast["objects"].each do |object|
207
+ language = object["language"]
208
+ next unless @_handlers[language]
209
+
210
+ @_objlangs[object["name"]] = language
211
+ @_handlers[language].load(object["name"], object["code"])
212
+ end
213
+
214
+ ok
215
+ end
216
+
217
+ # Populate sort buffers after loading RiveScript code.
218
+ def sort_replies
219
+ @_sorted = { "topics" => {}, "thats" => {} }
220
+ say("Sorting triggers...")
221
+
222
+ @_topics.each_key do |topic|
223
+ say("Analyzing topic #{topic}...")
224
+
225
+ all_triggers = Inheritance.get_topic_triggers(self, topic)
226
+ @_sorted["topics"][topic] = Sorting.sort_trigger_set(all_triggers, true)
227
+
228
+ that_triggers = Inheritance.get_topic_triggers(self, topic, true)
229
+ @_sorted["thats"][topic] = Sorting.sort_trigger_set(that_triggers, false)
230
+ end
231
+
232
+ @_sorted["sub"] = Sorting.sort_list(@_sub.keys)
233
+ @_sorted["person"] = Sorting.sort_list(@_person.keys)
234
+ @_sorted
235
+ end
236
+
237
+ # Translate the in-memory brain into a JSON-serializable structure.
238
+ def deparse
239
+ result = {
240
+ "begin" => {
241
+ "global" => Utils.clone(@_global),
242
+ "var" => Utils.clone(@_var),
243
+ "sub" => Utils.clone(@_sub),
244
+ "person" => Utils.clone(@_person),
245
+ "array" => Utils.clone(@_array),
246
+ "triggers" => []
247
+ },
248
+ "topics" => Utils.clone(@_topics),
249
+ "inherits" => Utils.clone(@_inherits),
250
+ "includes" => Utils.clone(@_includes),
251
+ "objects" => {}
252
+ }
253
+
254
+ @_handlers.each do |key, handler|
255
+ next unless handler.respond_to?(:objects)
256
+
257
+ entry = { "_objects" => Utils.clone(handler.objects) }
258
+ entry["_sources"] = Utils.clone(handler.sources) if handler.respond_to?(:sources)
259
+ result["objects"][key] = entry
260
+ end
261
+
262
+ if result["topics"]["__begin__"]
263
+ result["begin"]["triggers"] = result["topics"].delete("__begin__")
264
+ end
265
+
266
+ result["begin"]["global"]["debug"] = @_debug if @_debug
267
+ result["begin"]["global"]["depth"] = @_depth if @_depth != 50
268
+
269
+ result
270
+ end
271
+
272
+ # Translate the in-memory brain back into RiveScript source code.
273
+ def stringify(deparsed = nil)
274
+ @parser.stringify(deparsed || deparse)
275
+ end
276
+
277
+ # Write the in-memory RiveScript data into a text file.
278
+ def write(filename, deparsed = nil)
279
+ File.write(filename, stringify(deparsed))
280
+ end
281
+
282
+ # Set a custom language handler for object macros.
283
+ # Omit +obj+ to remove the handler; pass +nil+ to disable it explicitly.
284
+ def set_handler(lang, obj = :__unset__)
285
+ if obj == :__unset__
286
+ @_handlers.delete(lang)
287
+ else
288
+ @_handlers[lang] = obj
289
+ end
290
+ end
291
+
292
+ # Define a Ruby object macro from your program.
293
+ def set_subroutine(name, code = nil, &block)
294
+ code ||= block
295
+ return unless @_handlers["ruby"]
296
+ return if code.nil?
297
+
298
+ @_objlangs[name] = "ruby"
299
+ @_handlers["ruby"].load(name, code)
300
+ end
301
+
302
+ # Set a global variable (! global).
303
+ def set_global(name, value = :__unset__)
304
+ if value == :__unset__
305
+ @_global.delete(name)
306
+ else
307
+ @_global[name] = value
308
+ end
309
+ end
310
+
311
+ # Set a bot variable (! var).
312
+ def set_variable(name, value = :__unset__)
313
+ if value == :__unset__
314
+ @_var.delete(name)
315
+ else
316
+ @_var[name] = value
317
+ end
318
+ end
319
+
320
+ # Set a substitution (! sub).
321
+ def set_substitution(name, value = :__unset__)
322
+ if value == :__unset__
323
+ @_sub.delete(name)
324
+ else
325
+ @_submax = [name.split(" ").length, @_submax].max
326
+ @_sub[name] = value
327
+ end
328
+ end
329
+
330
+ # Set a person substitution (! person).
331
+ def set_person(name, value = :__unset__)
332
+ if value == :__unset__
333
+ @_person.delete(name)
334
+ else
335
+ @_personmax = [name.split(" ").length, @_personmax].max
336
+ @_person[name] = value
337
+ end
338
+ end
339
+
340
+ # Set a user variable.
341
+ def set_uservar(user, name, value)
342
+ value = value.downcase if name == "topic" && @_force_case
343
+
344
+ @_session.set(user, { name => value })
345
+ end
346
+
347
+ # Set multiple user variables at once.
348
+ def set_uservars(user, data)
349
+ @_session.set(user, data)
350
+ end
351
+
352
+ # Get a bot variable (<bot name>).
353
+ def get_variable(name)
354
+ @_var.key?(name) ? @_var[name] : "undefined"
355
+ end
356
+
357
+ # Get a user variable.
358
+ def get_uservar(user, name)
359
+ @_session.get(user, name)
360
+ end
361
+
362
+ # Get all variables about a user, or all users if +user+ is omitted.
363
+ def get_uservars(user = nil)
364
+ if user.nil?
365
+ @_session.get_all
366
+ else
367
+ @_session.get_any(user)
368
+ end
369
+ end
370
+
371
+ # Clear user variables for one user, or all users if +user+ is omitted.
372
+ def clear_uservars(user = nil)
373
+ if user.nil?
374
+ @_session.reset_all
375
+ else
376
+ @_session.reset(user)
377
+ end
378
+ end
379
+
380
+ # Freeze the variable state of a user.
381
+ def freeze_uservars(user)
382
+ @_session.freeze(user)
383
+ end
384
+
385
+ # Thaw a user's frozen variables.
386
+ def thaw_uservars(user, action = "thaw")
387
+ @_session.thaw(user, action)
388
+ end
389
+
390
+ # Retrieve the trigger the user matched most recently.
391
+ def last_match(user)
392
+ @_session.get(user, "__lastmatch__")
393
+ end
394
+
395
+ # Retrieve the trigger the user matched initially for the last reply.
396
+ def initial_match(user)
397
+ @_session.get(user, "__initialmatch__")
398
+ end
399
+
400
+ # Retrieve all triggers matched for the last reply.
401
+ def last_triggers(user)
402
+ @_session.get(user, "__last_triggers__")
403
+ end
404
+
405
+ # Retrieve triggers in the current topic for the specified user.
406
+ def get_user_topic_triggers(user)
407
+ topic = @_session.get(user, "topic")
408
+ Inheritance.get_topic_triggers(self, topic)
409
+ end
410
+
411
+ # Retrieve the current user's ID (only valid inside object macros).
412
+ def current_user
413
+ if @brain.current_user.nil?
414
+ warn("currentUser() is intended to be called from within a Ruby object macro!")
415
+ end
416
+ @brain.current_user
417
+ end
418
+
419
+ # Fetch a reply from the RiveScript brain.
420
+ def reply(user, msg, scope = nil)
421
+ @brain.reply(user, msg, scope)
422
+ end
423
+
424
+ # Deprecated alias for reply.
425
+ def reply_async(user, msg, scope = nil, &block)
426
+ warn("DEPRECATED FUNCTION: RiveScript#reply_async is deprecated; use reply instead")
427
+ result = reply(user, msg, scope)
428
+ block&.call(nil, result)
429
+ result
430
+ end
431
+
432
+ # Debug logger.
433
+ def say(message)
434
+ return unless @_debug
435
+
436
+ if @on_debug
437
+ @on_debug.call(message)
438
+ else
439
+ $stdout.puts(message)
440
+ end
441
+ end
442
+
443
+ # Warning/error logger.
444
+ def warn(message, filename = nil, lineno = nil)
445
+ message = "#{message} at #{filename} line #{lineno}" if filename && lineno
446
+ formatted = "[WARNING] #{message}"
447
+
448
+ if @on_debug
449
+ @on_debug.call(formatted)
450
+ else
451
+ $stderr.puts(formatted)
452
+ end
453
+ end
454
+
455
+ private
456
+
457
+ def runtime
458
+ "ruby"
459
+ end
460
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rivescript
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Byte
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-05 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: RiveScript is a scripting language for chatterbots. This gem is a Ruby
14
+ 3.3 port of the RiveScript interpreter.
15
+ email:
16
+ - byte@jvmlab.org
17
+ executables:
18
+ - riveshell
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - Changes.md
23
+ - LICENSE
24
+ - README.md
25
+ - bin/riveshell
26
+ - eg/brain/admin.rive
27
+ - eg/brain/begin.rive
28
+ - eg/brain/clients.rive
29
+ - eg/brain/eliza.rive
30
+ - eg/brain/myself.rive
31
+ - eg/brain/rpg.rive
32
+ - lib/rivescript.rb
33
+ - lib/rivescript/brain.rb
34
+ - lib/rivescript/inheritance.rb
35
+ - lib/rivescript/lang/ruby.rb
36
+ - lib/rivescript/parser.rb
37
+ - lib/rivescript/sessions.rb
38
+ - lib/rivescript/sorting.rb
39
+ - lib/rivescript/utils.rb
40
+ - lib/rivescript/version.rb
41
+ homepage: https://github.com/LilOleByte/rivescript-rb
42
+ licenses:
43
+ - MIT
44
+ metadata:
45
+ homepage_uri: https://jvmlab.org/
46
+ source_code_uri: https://github.com/LilOleByte/rivescript-rb
47
+ changelog_uri: https://github.com/LilOleByte/rivescript-rb/blob/main/Changes.md
48
+ bug_tracker_uri: https://github.com/LilOleByte/rivescript-rb/issues
49
+ rubygems_mfa_required: 'true'
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 3.3.0
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubygems_version: 3.5.22
66
+ signing_key:
67
+ specification_version: 4
68
+ summary: RiveScript interpreter for Ruby
69
+ test_files: []