james 0.5.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.
@@ -0,0 +1,54 @@
1
+ require File.expand_path '../../../lib/james', __FILE__
2
+
3
+ module James
4
+
5
+ class CLI
6
+
7
+ def execute *patterns
8
+ options = extract_options patterns
9
+
10
+ dialogs = find_dialogs_for patterns
11
+
12
+ puts "James: I haven't found anything to talk about (No files found). Exiting." or exit!(1) if dialogs.empty?
13
+ puts "James: Using dialogs in #{dialogs.join(', ')} for our conversation, Sir."
14
+
15
+ load_all dialogs
16
+
17
+ James.listen options
18
+ end
19
+
20
+ # Defines default options and extracts options from
21
+ # command line.
22
+ #
23
+ # Sadly needs to be run before processing the dialog file names.
24
+ #
25
+ def extract_options patterns
26
+ silent = patterns.delete '-s'
27
+ silent_input = patterns.delete '-si'
28
+ silent_output = patterns.delete '-so'
29
+
30
+ options = {}
31
+ options[:input] = Inputs::Terminal if silent || silent_input
32
+ options[:output] = Outputs::Terminal if silent || silent_output
33
+
34
+ options
35
+ end
36
+
37
+ #
38
+ #
39
+ def find_dialogs_for patterns
40
+ patterns = ["**/*_dialog{,ue}.rb"] if patterns.empty?
41
+ Dir[*patterns]
42
+ end
43
+
44
+ #
45
+ #
46
+ def load_all dialogs
47
+ dialogs.each do |dialog|
48
+ load File.expand_path dialog, Dir.pwd
49
+ end
50
+ end
51
+
52
+ end
53
+
54
+ end
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+
4
+ # james joke_dialog.rb phonebook_dialog.rb quote_dialog.rb # Uses just the described ones.
5
+ # OR
6
+ # james j*.rb # Uses all the dialogs matching the pattern.
7
+ # OR
8
+ # james # Uses all dialogs it can find in this dir and subdirs.
9
+ #
10
+ # Options:
11
+ # * -s # Silent input and output (same as "-si -so").
12
+ # * -si # Silent input.
13
+ # * -so # Silent output.
14
+ #
15
+
16
+ begin
17
+ require 'james/cli'
18
+ rescue LoadError => e
19
+ require 'rubygems'
20
+ james_path = File.expand_path '../../aux', __FILE__
21
+ $:.unshift(james_path) if File.directory?(james_path) && !$:.include?(james_path)
22
+ require 'james/cli'
23
+ end
24
+
25
+ cli = James::CLI.new
26
+ cli.execute *ARGV
@@ -0,0 +1,50 @@
1
+ module James; end
2
+
3
+ require File.expand_path '../james/state_api', __FILE__
4
+ require File.expand_path '../james/state_internals', __FILE__
5
+
6
+ require File.expand_path '../james/markers/marker', __FILE__
7
+ require File.expand_path '../james/markers/current', __FILE__
8
+ require File.expand_path '../james/markers/memory', __FILE__
9
+ require File.expand_path '../james/conversation', __FILE__
10
+
11
+ require File.expand_path '../james/dialog_api', __FILE__
12
+ require File.expand_path '../james/dialog_internals', __FILE__
13
+
14
+ require File.expand_path '../james/dialogs', __FILE__
15
+
16
+ require File.expand_path '../james/inputs/base', __FILE__
17
+ require File.expand_path '../james/inputs/audio', __FILE__
18
+ require File.expand_path '../james/inputs/terminal', __FILE__
19
+
20
+ require File.expand_path '../james/outputs/audio', __FILE__
21
+ require File.expand_path '../james/outputs/terminal', __FILE__
22
+
23
+ require File.expand_path '../james/builtin/core_dialog', __FILE__
24
+
25
+ require File.expand_path '../james/framework', __FILE__
26
+ require File.expand_path '../james/controller', __FILE__
27
+
28
+ module James
29
+
30
+ # Use the given dialogs.
31
+ #
32
+ # If called twice or more, will just add more dialogs.
33
+ #
34
+ def self.use *dialogs
35
+ dialogs.each { |dialog| controller << dialog}
36
+ end
37
+
38
+ # Start listening.
39
+ #
40
+ def self.listen options
41
+ controller.listen options
42
+ end
43
+
44
+ # Controller instance.
45
+ #
46
+ def self.controller
47
+ Controller.instance
48
+ end
49
+
50
+ end
@@ -0,0 +1,52 @@
1
+ # This is the core dialog every dialog will be hooking into.
2
+ #
3
+ # Eventually, the design should be such that everyone can use
4
+ # the design and core dialog they like best.
5
+ #
6
+ # But to get going, this suffices for now.
7
+ #
8
+ class CoreDialog
9
+
10
+ include James::Dialog
11
+
12
+ # This core dialog starts at awake.
13
+ #
14
+ initially :awake
15
+
16
+ # The alert state.
17
+ # When James is in this state, he should be
18
+ # open for user dialogs.
19
+ #
20
+ state :awake do
21
+ # If James is awake, he offers more dialogs
22
+ # on this state, if there are any hooked into this state.
23
+ #
24
+ chainable
25
+
26
+ hear "Thank you, James." => :awake,
27
+ 'I need some time alone, James.' => :away,
28
+ "Good night, James." => :exit
29
+ into { "Sir?" }
30
+ end
31
+
32
+ # The away state. James does not listen to any
33
+ # user dialog hooks, but only for his name
34
+ # or the good night, i.e. exit phrase.
35
+ #
36
+ state :away do
37
+ hear 'James?' => :awake,
38
+ "Good night, James." => :exit
39
+ into { "Of course, Sir!" }
40
+ end
41
+
42
+ # This is not a real state. It just exists to Kernel.exit
43
+ # James when he enters this state.
44
+ #
45
+ state :exit do
46
+ into do
47
+ puts "James: Exits through a side door."
48
+ Kernel.exit
49
+ end
50
+ end
51
+
52
+ end
@@ -0,0 +1,106 @@
1
+ module James
2
+
3
+ class Controller
4
+
5
+ attr_reader :conversation, :listening, :initial
6
+
7
+ # Singleton reader.
8
+ #
9
+ def self.instance
10
+ @controller ||= new
11
+ end
12
+
13
+ # This puts together the initial dialog and the user
14
+ # ones that are hooked into it.
15
+ #
16
+ # The initial dialog needs an state defined as initially.
17
+ # This is where it will start.
18
+ #
19
+ # Example:
20
+ # initially :awake
21
+ # state :awake do
22
+ # # ...
23
+ # end
24
+ #
25
+ # If you don't give it an initial dialog,
26
+ # James will simply use the built-in CoreDialog.
27
+ #
28
+ def initialize dialog = nil
29
+ @initial = dialog || CoreDialog.new
30
+ @conversation = Conversation.new @initial.current
31
+ end
32
+
33
+ # Convenience method to add a dialog to the current system.
34
+ #
35
+ # Will add the dialog to the initial dialog passed into the
36
+ # controller.
37
+ #
38
+ def << dialog
39
+ @initial << dialog
40
+ end
41
+
42
+ # Start listening using the provided options.
43
+ #
44
+ # Options:
45
+ # * input # Inputs::Terminal or Inputs::Audio (default).
46
+ # * output # Outputs::Terminal or Outputs::Audio (default).
47
+ #
48
+ def listen options = {}
49
+ return if listening
50
+
51
+ @listening = true
52
+
53
+ @input_class = options[:input] || Inputs::Audio
54
+ @output_class = options[:output] || Outputs::Audio
55
+
56
+ @output_options ||= {}
57
+ @output_options[:voice] = options[:voice] || 'com.apple.speech.synthesis.voice.Alex'
58
+
59
+ app = NSApplication.sharedApplication
60
+ app.delegate = self
61
+
62
+ app.run
63
+ end
64
+
65
+ # The naughty privates of this class.
66
+ #
67
+
68
+ # MacRuby callback functions.
69
+ #
70
+ def applicationDidFinishLaunching notification
71
+ start_output
72
+ start_input
73
+ end
74
+ def windowWillClose notification
75
+ exit
76
+ end
77
+
78
+ # Start recognizing words.
79
+ #
80
+ def start_input
81
+ @input = @input_class.new self
82
+ @input.listen
83
+ end
84
+ # Start speaking.
85
+ #
86
+ def start_output
87
+ @output = @output_class.new @output_options
88
+ end
89
+
90
+ # Callback method from dialog.
91
+ #
92
+ def say text
93
+ @output.say text
94
+ end
95
+ def hear text
96
+ conversation.hear text do |response|
97
+ say response
98
+ end
99
+ end
100
+ def expects
101
+ conversation.expects
102
+ end
103
+
104
+ end
105
+
106
+ end
@@ -0,0 +1,62 @@
1
+ module James
2
+
3
+ # A conversation has a number of markers (position in dialog),
4
+ # whose dialogs are visited in order of preference.
5
+ #
6
+ # Why?
7
+ # Conversations have multiple points where they can be.
8
+ # (Politics, then this joke, then back again, finally "Oh, bye I have to go!")
9
+ #
10
+ class Conversation
11
+
12
+ attr_accessor :markers
13
+
14
+ # A Conversation keeps a stack of markers with
15
+ # an initial one.
16
+ #
17
+ def initialize initial
18
+ @markers = [initial]
19
+ end
20
+
21
+ # Hear tries all visitors in order
22
+ # until one hears a phrase he knows.
23
+ #
24
+ # If a dialog boundary has been crossed:
25
+ # A new visitor is added with the target
26
+ # state of that heard phrase at the position.
27
+ #
28
+ # After that, all remaining visitors are
29
+ # removed from the current stack (since
30
+ # we are obviously not in one of the later
31
+ # dialogs anymore).
32
+ #
33
+ def hear phrase, &block
34
+ self.markers = markers.inject([]) do |remaining, marker|
35
+ markers = marker.hear phrase, &block
36
+ remaining = remaining + markers
37
+ break remaining if remaining.last && remaining.last.current?
38
+ remaining
39
+ end
40
+ end
41
+
42
+ # Enter enters the first visitor.
43
+ #
44
+ def enter
45
+ markers.first.enter
46
+ end
47
+
48
+ # Simply returns the sum of what phrases all dialogs do expect, front-to-back.
49
+ #
50
+ # Stops as soon as a marker is not on a chainable state anymore.
51
+ #
52
+ def expects
53
+ markers.inject([]) do |expects, marker|
54
+ total = marker.expects + expects
55
+ break total unless marker.chainable?
56
+ total
57
+ end
58
+ end
59
+
60
+ end
61
+
62
+ end
@@ -0,0 +1,38 @@
1
+ module James
2
+
3
+ # A dialog can be instantiated in two ways:
4
+ #
5
+ # The simple way will directly add itself to James.
6
+ #
7
+ # James.dialog(optional_args_for_initialize) do
8
+ # # Your dialog.
9
+ # #
10
+ # end
11
+ #
12
+ # class MyDialog
13
+ #
14
+ # include James::Dialog
15
+ #
16
+ # # Your dialog definition.
17
+ # #
18
+ #
19
+ # end
20
+ #
21
+ # # Tell James to use the dialog.
22
+ # #
23
+ # James.use MyDialog.new
24
+ #
25
+ module Dialog; end
26
+
27
+ class << self
28
+
29
+ def dialog *args, &block
30
+ dialog = Class.new { include James::Dialog }
31
+ dialog.class_eval &block
32
+ use dialog.new(*args)
33
+ dialog
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,105 @@
1
+ module James
2
+
3
+ # A dialog is just a container object
4
+ # for defining states and executing methods.
5
+ #
6
+ module Dialog
7
+
8
+ def self.included into
9
+ into.extend ClassMethods
10
+ end
11
+
12
+ # Returns a state instance for the given state / or state name.
13
+ #
14
+ # Note: Lazily creates the state instances.
15
+ #
16
+ def state_for possible_state
17
+ return possible_state if possible_state.respond_to?(:expects)
18
+ self.class.state_for possible_state, self
19
+ end
20
+
21
+ # Chain (the states of) this dialog to the given state.
22
+ #
23
+ # Creates state instances if it is given names.
24
+ #
25
+ # Note: Be careful not to create circular
26
+ # state chaining. Except if you really
27
+ # want that.
28
+ #
29
+ def chain_to state
30
+ warn "Define a hear => :some_state_name in a dialog to have it be able to chain to another." && return unless respond_to?(:entry_phrases)
31
+ entry_phrases.each do |(phrases, entry_state)|
32
+ state.hear phrases => state_for(entry_state)
33
+ end
34
+ end
35
+
36
+ # Chain the given Dialog(s) to all chainable
37
+ # states in this Dialog.
38
+ #
39
+ # Note: If you only want one state to chain,
40
+ # then get it from the otiginating dialog
41
+ # using dialog.state_for(:name) and
42
+ # append the dialog there:
43
+ # dialog.follows preceding_dialog.state_for(:name)
44
+ #
45
+ def << dialog_s
46
+ self.class.states.each do |(name, definition)|
47
+ state = state_for name # TODO Do not call this everywhere.
48
+ dialog_s.chain_to(state) if state.chainable?
49
+ end
50
+ end
51
+
52
+ module ClassMethods
53
+
54
+ def initially state_name
55
+ define_method :current do
56
+ Markers::Current.new state_for(state_name)
57
+ end
58
+ end
59
+
60
+ # Defines the entry phrases into this dialog.
61
+ #
62
+ # Example:
63
+ # hear 'Hello, James!' => :start
64
+ #
65
+ def hear definition
66
+ define_method :entry_phrases do
67
+ definition
68
+ end
69
+ end
70
+
71
+ # Defines a state with transitions.
72
+ #
73
+ # Example:
74
+ # state :name do
75
+ # # state properties (hear, into, exit) go here.
76
+ # end
77
+ #
78
+ def state name, &block
79
+ @states ||= {}
80
+ @states[name] ||= block if block_given?
81
+ define_method name do
82
+ state_for name
83
+ end unless instance_methods.include? name
84
+ end
85
+
86
+ #
87
+ #
88
+ attr_reader :states
89
+
90
+ # Return a state for this name (and dialog instance).
91
+ #
92
+ def state_for name, instance
93
+ # Lazily wrap in State instance.
94
+ #
95
+ if states[name].respond_to?(:call)
96
+ states[name] = State.new(name, instance, &states[name])
97
+ end
98
+ states[name]
99
+ end
100
+
101
+ end
102
+
103
+ end
104
+
105
+ end
@@ -0,0 +1,21 @@
1
+ module James
2
+
3
+ # Bundles a bunch of dialogs.
4
+ #
5
+ class Dialogs
6
+
7
+ attr_reader :dialogs
8
+
9
+ def initialize *dialogs
10
+ @dialogs = dialogs
11
+ end
12
+
13
+ #
14
+ #
15
+ def chain_to incoming_dialog
16
+ dialogs.each { |dialog| incoming_dialog << dialog }
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1 @@
1
+ framework 'AppKit'
@@ -0,0 +1,47 @@
1
+ module James
2
+
3
+ module Inputs
4
+
5
+ class Audio < Base
6
+
7
+ def initialize controller
8
+ super controller
9
+ @recognizer = NSSpeechRecognizer.alloc.init
10
+ @recognizer.setBlocksOtherRecognizers true
11
+ @recognizer.setListensInForegroundOnly false
12
+ @recognizer.setDelegate self
13
+ end
14
+
15
+ def listen
16
+ @recognizer.startListening
17
+ recognize_new_commands
18
+ end
19
+ def heard command
20
+ super
21
+
22
+ # Set recognizable commands.
23
+ #
24
+ recognize_new_commands
25
+ end
26
+
27
+ # Callback method from the speech interface.
28
+ #
29
+ # Note: Uses a MacRuby only form.
30
+ #
31
+ def speechRecognizer sender, didRecognizeCommand: command
32
+ heard command
33
+ end
34
+ def recognize_new_commands
35
+ possibilities = controller.expects
36
+ puts "Possibilities:\n"
37
+ possibilities.each_with_index do |possibility, index|
38
+ puts "#{index + 1}) #{possibility}"
39
+ end
40
+ @recognizer.setCommands possibilities
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,32 @@
1
+ module James
2
+
3
+ module Inputs
4
+
5
+ class Base
6
+
7
+ attr_reader :controller
8
+
9
+ def initialize controller
10
+ @controller = controller
11
+ end
12
+
13
+ # Call this method if you heard something in the subclass.
14
+ #
15
+ def heard command
16
+ controller.hear command
17
+ end
18
+
19
+ # Shows possible commands in the terminal.
20
+ #
21
+ def show_possibilities possibilities
22
+ puts "Possibilities:\n"
23
+ possibilities.each_with_index do |possibility, index|
24
+ puts "#{index + 1}) #{possibility}"
25
+ end
26
+ end
27
+
28
+ end
29
+
30
+ end
31
+
32
+ end
@@ -0,0 +1,37 @@
1
+ module James
2
+
3
+ module Inputs
4
+
5
+ # Terminal input for silent purposes.
6
+ #
7
+ class Terminal < Base
8
+
9
+ # Start listening to commands by the user.
10
+ #
11
+ def listen
12
+ sleep 2
13
+ loop do
14
+ possibilities = controller.expects
15
+ show_possibilities possibilities
16
+ command = get_command
17
+ puts "I heard '#{command}'."
18
+ command = possibilities[command.to_i-1] unless command.to_i.zero?
19
+ heard command if possibilities.include? command
20
+ end
21
+ end
22
+
23
+ # Get the next command by the user.
24
+ #
25
+ def get_command
26
+ STDIN.gets.chop
27
+ rescue IOError
28
+ puts "Wait a second, please, Sir, I am busy."
29
+ sleep 1
30
+ retry
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+
37
+ end
@@ -0,0 +1,34 @@
1
+ module James
2
+
3
+ module Markers
4
+
5
+ # The visitor knows where in the conversation we are.
6
+ #
7
+ class Current < Marker
8
+
9
+ # Hear a phrase.
10
+ #
11
+ # Returns a new marker and self if it crossed a boundary.
12
+ # Returns itself if not.
13
+ #
14
+ def hear phrase, &block
15
+ return [self] unless hears? phrase
16
+ last = current
17
+ process(phrase, &block) ? [Memory.new(last), self] : [self]
18
+ end
19
+
20
+ # Expects all phrases, not just internal.
21
+ #
22
+ def expects
23
+ current.expects
24
+ end
25
+
26
+ def current?
27
+ true
28
+ end
29
+
30
+ end
31
+
32
+ end
33
+
34
+ end
@@ -0,0 +1,94 @@
1
+ module James
2
+
3
+ module Markers
4
+
5
+ # A marker is a point in conversation
6
+ # where we once were and might go back.
7
+ #
8
+ # TODO: Rename to ?.
9
+ #
10
+ class Marker
11
+
12
+ attr_accessor :current
13
+
14
+ # Pass in an current state.
15
+ #
16
+ def initialize current
17
+ @current = current
18
+ end
19
+
20
+ # Resets the current state back to the initial.
21
+ #
22
+ def reset
23
+ # Never moves, thus never reset.
24
+ end
25
+
26
+ # We hear a phrase.
27
+ #
28
+ # Also used to start the whole process.
29
+ #
30
+ def enter
31
+ result = current.__into__
32
+ yield result if result && block_given?
33
+ result
34
+ end
35
+
36
+ #
37
+ #
38
+ def exit
39
+ result = current.__exit__
40
+ yield result if result && block_given?
41
+ result
42
+ end
43
+
44
+ #
45
+ #
46
+ def transition phrase
47
+ state_or_lambda = current.next_for phrase
48
+ if state_or_lambda.respond_to?(:call)
49
+ result = current.__transition__ &state_or_lambda # Don't transition.
50
+ yield result if result && block_given?
51
+ result
52
+ else
53
+ self.current = state_or_lambda
54
+ end
55
+ end
56
+
57
+ #
58
+ #
59
+ def check
60
+ yield("Whoops. That led nowhere. Perhaps you didn't define the target state?") unless self.current
61
+ end
62
+
63
+ # Returns falsy if it stays the same.
64
+ #
65
+ def process phrase, &block
66
+ exit_text = exit &block
67
+ last_context = current.context
68
+ transition phrase, &block
69
+ check &block
70
+ into_text = enter &block
71
+ last_context != current.context
72
+ end
73
+
74
+ #
75
+ #
76
+ def hears? phrase
77
+ expects.include? phrase
78
+ end
79
+
80
+ # Does the current state allow penetration into another dialog?
81
+ #
82
+ def chainable?
83
+ current.chainable?
84
+ end
85
+
86
+ def to_s
87
+ "#{self.class.name}(#{initial}, current: #{current})"
88
+ end
89
+
90
+ end
91
+
92
+ end
93
+
94
+ end
@@ -0,0 +1,37 @@
1
+ module James
2
+
3
+ module Markers
4
+
5
+ # A marker is a point in conversation
6
+ # where we once were and might go back.
7
+ #
8
+ # TODO: Rename to ?.
9
+ #
10
+ class Memory < Marker
11
+
12
+ # Hear a phrase.
13
+ #
14
+ # Returns a new Current if it heard.
15
+ # Returns itself if not.
16
+ #
17
+ def hear phrase, &block
18
+ return [self] unless hears? phrase
19
+ last = current
20
+ process(phrase, &block) ? [Memory.new(last), Current.new(current)] : [Current.new(current)]
21
+ end
22
+
23
+ # A marker does not care about phrases that cross dialog boundaries.
24
+ #
25
+ def expects
26
+ current.internal_expects
27
+ end
28
+
29
+ def current?
30
+ false
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+
37
+ end
@@ -0,0 +1,31 @@
1
+ module James
2
+
3
+ module Outputs
4
+
5
+ class Audio
6
+
7
+ # Create a new audio output.
8
+ #
9
+ # Options:
10
+ # * voice # Default is 'com.apple.speech.synthesis.voice.Alex'.
11
+ #
12
+ def initialize options = {}
13
+ @output = NSSpeechSynthesizer.alloc.initWithVoice options[:voice] || 'com.apple.speech.synthesis.voice.Alex'
14
+ end
15
+
16
+ # Say the given text out loud.
17
+ #
18
+ # Waits for the last text to be finished
19
+ #
20
+ def say text
21
+ while @output.isSpeaking
22
+ sleep 0.1
23
+ end
24
+ @output.startSpeakingString text
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,27 @@
1
+ module James
2
+
3
+ module Outputs
4
+
5
+ # Terminal output for silent purposes.
6
+ #
7
+ class Terminal
8
+
9
+ #
10
+ #
11
+ def initialize options = {}
12
+
13
+ end
14
+
15
+ # Say the given text in the terminal.
16
+ #
17
+ def say text
18
+ puts
19
+ p text
20
+ puts
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1,123 @@
1
+ module James
2
+
3
+ # A state is defined in a dialog.
4
+ #
5
+ # It has a name with which it can be targeted.
6
+ #
7
+ # A state has three methods:
8
+ # * hear: If this phrase (or one of these phrases) is heard, move to that state. Takes a hash.
9
+ # * into: A block that is called on entering.
10
+ # * exit: A block that is called on exit.
11
+ #
12
+ # Example:
13
+ # state :time do
14
+ # hear ['What time is it?', 'And now?'] => :time
15
+ # into { time = Time.now; "It is currently #{time.hour} #{time.min}." }
16
+ # exit { "And that was the time." }
17
+ # end
18
+ #
19
+ class State
20
+
21
+ attr_reader :name, :context
22
+
23
+ def initialize name, context
24
+ @name = name
25
+ @context = context
26
+
27
+ @transitions = {}
28
+
29
+ instance_eval(&Proc.new) if block_given?
30
+ end
31
+
32
+ # How do I get from this state to another?
33
+ #
34
+ # Example:
35
+ # hear 'What time is it?' => :time,
36
+ # 'What? This late?' => :yes
37
+ #
38
+ # Example for staying in the same state:
39
+ # hear 'What time is it?' # Implicitly staying.
40
+ #
41
+ # Example for staying in the same state and doing something:
42
+ # hear 'What time is it?' => ->() { "I'm staying in the same state" }
43
+ #
44
+ def hear transitions
45
+ transitions = { transitions => name } unless transitions.respond_to?(:to_hash)
46
+ @transitions = expand(transitions).merge @transitions
47
+ end
48
+
49
+ # Execute this block or say the text when entering this state.
50
+ #
51
+ # Examples:
52
+ # into "Yes, Sir?"
53
+ # into { "A random number is #{rand(10)}" }
54
+ #
55
+ def into text = nil, &block
56
+ @into_block = block ||
57
+ text && lambda { text } ||
58
+ raise_no_text_or_block(__method__)
59
+ end
60
+
61
+ # Execute this block or say the text when exiting this state.
62
+ #
63
+ # Examples:
64
+ # exit "Yes, Sir?"
65
+ # exit { "A random number is #{rand(10)}" }
66
+ #
67
+ def exit text = nil, &block
68
+ @exit_block = block ||
69
+ text && lambda { text } ||
70
+ raise_no_text_or_block(__method__)
71
+ end
72
+
73
+ # Chain the given dialog to this state.
74
+ #
75
+ def << dialog
76
+ dialog.chain_to self
77
+ end
78
+
79
+ # A chainable state is a state from which other
80
+ # Dialogs can be reached.
81
+ #
82
+ def chainable
83
+ @chainable = true
84
+ end
85
+
86
+ # Description of self using name and transitions.
87
+ #
88
+ def to_s
89
+ "#{self.class.name}(#{name}, #{context}, #{@transitions})"
90
+ end
91
+
92
+ # The naughty privates of this class.
93
+ #
94
+
95
+ # Expands a hash in the form
96
+ # * [a, b] => c to a => c, b => c
97
+ # but leaves a non-array key alone.
98
+ #
99
+ def expand transitions
100
+ results = {}
101
+ transitions.each_pair do |phrases, state_name|
102
+ [*phrases].each do |phrase|
103
+ results[phrase] = state_name
104
+ end
105
+ end
106
+ results
107
+ end
108
+
109
+ # Raise an ArgumentError for the given method if it needs either a text or a block.
110
+ #
111
+ def raise_no_text_or_block on_method
112
+ raise ArgumentError.new("Neither block nor text given to ##{on_method} call in #{caller[1]}.")
113
+ end
114
+
115
+ # By default, a state is not chainable.
116
+ #
117
+ def chainable?
118
+ !!@chainable
119
+ end
120
+
121
+ end
122
+
123
+ end
@@ -0,0 +1,59 @@
1
+ module James
2
+
3
+ class State
4
+
5
+ # Transitions are internal transitions & external transitions.
6
+ #
7
+ def transitions
8
+ @transitions
9
+ end
10
+
11
+ #
12
+ #
13
+ def expects
14
+ transitions.keys
15
+ end
16
+
17
+ #
18
+ #
19
+ def internal_expects
20
+ transitions.select { |phrase, target| target.respond_to?(:to_sym) || target == self.context }.keys
21
+ end
22
+
23
+
24
+ # Returns the next state for the given phrase.
25
+ #
26
+ # It accesses the context (aka Dialog) to get a full object state.
27
+ #
28
+ # If it is a Symbol, James will try to get the real state.
29
+ # If not, it will just return it (a State already, or lambda).
30
+ #
31
+ def next_for phrase
32
+ state = self.transitions[phrase]
33
+ state.respond_to?(:id2name) ? context.state_for(state) : state
34
+ end
35
+
36
+ # The naughty privates.
37
+ #
38
+
39
+ # Called by the visitor visiting this state.
40
+ #
41
+ def __into__
42
+ @into_block && context.instance_eval(&@into_block)
43
+ end
44
+
45
+ # Called by the visitor visiting this state.
46
+ #
47
+ def __exit__
48
+ @exit_block && context.instance_eval(&@exit_block)
49
+ end
50
+
51
+ # Called by the visitor visiting this state.
52
+ #
53
+ def __transition__ &block
54
+ context.instance_eval &block
55
+ end
56
+
57
+ end
58
+
59
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: james
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.5.0
6
+ platform: ruby
7
+ authors:
8
+ - Florian Hanke
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-06-15 00:00:00 +10:00
14
+ default_executable: james
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: rspec
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "0"
25
+ type: :development
26
+ version_requirements: *id001
27
+ description: Modular Electronic Butler. Using a simple dialog system where you can easily add more dialogs.
28
+ email: florian.hanke+james@gmail.com
29
+ executables:
30
+ - james
31
+ extensions: []
32
+
33
+ extra_rdoc_files: []
34
+
35
+ files:
36
+ - lib/james/builtin/core_dialog.rb
37
+ - lib/james/controller.rb
38
+ - lib/james/conversation.rb
39
+ - lib/james/dialog_api.rb
40
+ - lib/james/dialog_internals.rb
41
+ - lib/james/dialogs.rb
42
+ - lib/james/framework.rb
43
+ - lib/james/inputs/audio.rb
44
+ - lib/james/inputs/base.rb
45
+ - lib/james/inputs/terminal.rb
46
+ - lib/james/markers/current.rb
47
+ - lib/james/markers/marker.rb
48
+ - lib/james/markers/memory.rb
49
+ - lib/james/outputs/audio.rb
50
+ - lib/james/outputs/terminal.rb
51
+ - lib/james/state_api.rb
52
+ - lib/james/state_internals.rb
53
+ - lib/james.rb
54
+ - aux/james/cli.rb
55
+ - bin/james
56
+ has_rdoc: true
57
+ homepage: http://floere.github.com/james
58
+ licenses: []
59
+
60
+ post_install_message:
61
+ rdoc_options: []
62
+
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: "0"
77
+ requirements: []
78
+
79
+ rubyforge_project:
80
+ rubygems_version: 1.5.0
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: "James: Modular Electronic Butler with modular Dialogs."
84
+ test_files: []
85
+