james 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+