james 0.1.1-universal-darwin-10 → 0.2.0-universal-darwin-10

Sign up to get free protection for your applications and to get access to all the features.
data/aux/james/cli.rb CHANGED
@@ -5,9 +5,7 @@ module James
5
5
  class CLI
6
6
 
7
7
  def execute *patterns
8
- silent = patterns.delete '-s'
9
- silent_input = patterns.delete '-si'
10
- silent_output = patterns.delete '-so'
8
+ options = extract_options patterns
11
9
 
12
10
  dialogs = find_dialogs_for patterns
13
11
 
@@ -16,16 +14,35 @@ module James
16
14
 
17
15
  load_all dialogs
18
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
+
19
30
  options = {}
20
31
  options[:input] = Inputs::Terminal if silent || silent_input
21
32
  options[:output] = Outputs::Terminal if silent || silent_output
22
33
 
23
- James.listen options
34
+ options
24
35
  end
36
+
37
+ #
38
+ #
25
39
  def find_dialogs_for patterns
26
40
  patterns = ["**/*_dialog{,ue}.rb"] if patterns.empty?
27
41
  Dir[*patterns]
28
42
  end
43
+
44
+ #
45
+ #
29
46
  def load_all dialogs
30
47
  dialogs.each do |dialog|
31
48
  load File.expand_path dialog, Dir.pwd
@@ -9,6 +9,10 @@ class CoreDialog
9
9
 
10
10
  include James::Dialog
11
11
 
12
+ # This core dialog starts at awake.
13
+ #
14
+ initially :awake
15
+
12
16
  # The alert state.
13
17
  # When James is in this state, he should be
14
18
  # open for user dialogs.
@@ -2,7 +2,7 @@ module James
2
2
 
3
3
  class Controller
4
4
 
5
- attr_reader :visitor, :listening
5
+ attr_reader :conversation, :listening, :initial
6
6
 
7
7
  # Singleton reader.
8
8
  #
@@ -10,63 +10,33 @@ module James
10
10
  @controller ||= new
11
11
  end
12
12
 
13
- # This puts together the core dialog and the user
13
+ # This puts together the initial dialog and the user
14
14
  # ones that are hooked into it.
15
15
  #
16
- # TODO Rewrite this. Design needs some refactoring.
17
- # Should the user visitor be created dynamically? (Probably yes.)
16
+ # The initial dialog needs an state defined as initially.
17
+ # This is where it will start.
18
18
  #
19
- def initialize
20
- @user_dialogs = Dialogs.new
21
- @visitor = Visitors.new system_visitor, user_visitor
22
- end
23
- def system_visitor
24
- Visitor.new CoreDialog.new.state_for(:awake)
25
- end
26
- def user_visitor
27
- @user_dialogs.visitor
28
- end
29
-
30
- # MacRuby callback functions.
19
+ # Example:
20
+ # initially :awake
21
+ # state :awake do
22
+ # # ...
23
+ # end
31
24
  #
32
- def applicationDidFinishLaunching notification
33
- start_output
34
- start_input
35
- end
36
- def windowWillClose notification
37
- exit
38
- end
39
-
40
- # Add a dialog to the current system.
25
+ # If you don't give it an initial dialog,
26
+ # James will simply use the built-in CoreDialog.
41
27
  #
42
- def add_dialog dialog
43
- @user_dialogs << dialog
28
+ def initialize dialog = nil
29
+ @initial = dialog || CoreDialog.new
30
+ @conversation = Conversation.new @initial.current
44
31
  end
45
32
 
46
- # Start recognizing words.
33
+ # Convenience method to add a dialog to the current system.
47
34
  #
48
- def start_input
49
- @input = @input_class.new self
50
- @input.listen
51
- end
52
- # Start speaking.
35
+ # Will add the dialog to the initial dialog passed into the
36
+ # controller.
53
37
  #
54
- def start_output
55
- @output = @output_class.new @output_options
56
- end
57
-
58
- # Callback method from dialog.
59
- #
60
- def say text
61
- @output.say text
62
- end
63
- def hear text
64
- visitor.hear text do |response|
65
- say response
66
- end
67
- end
68
- def expects
69
- visitor.expects
38
+ def << dialog
39
+ @initial << dialog
70
40
  end
71
41
 
72
42
  # Start listening using the provided options.
@@ -78,6 +48,8 @@ module James
78
48
  def listen options = {}
79
49
  return if listening
80
50
 
51
+ @listening = true
52
+
81
53
  @input_class = options[:input] || Inputs::Audio
82
54
  @output_class = options[:output] || Outputs::Audio
83
55
 
@@ -87,11 +59,48 @@ module James
87
59
  app = NSApplication.sharedApplication
88
60
  app.delegate = self
89
61
 
90
- @listening = true
91
-
92
62
  app.run
93
63
  end
94
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
+
95
104
  end
96
105
 
97
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
@@ -9,35 +9,88 @@ module James
9
9
  into.extend ClassMethods
10
10
  end
11
11
 
12
+ # Returns a state instance for the given state / or state name.
12
13
  #
14
+ # Note: Lazily creates the state instances.
13
15
  #
14
- def state_for name
15
- self.class.state_for name, self
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
16
50
  end
17
51
 
18
52
  module ClassMethods
19
53
 
20
- # Defines the entry sentences.
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
21
64
  #
22
65
  def hear definition
23
- define_method :entries do
66
+ define_method :entry_phrases do
24
67
  definition
25
68
  end
26
69
  end
27
70
 
28
71
  # Defines a state with transitions.
29
72
  #
30
- # state :name do
31
- # # state properties (hear, into, exit) go here.
32
- # end
73
+ # Example:
74
+ # state :name do
75
+ # # state properties (hear, into, exit) go here.
76
+ # end
33
77
  #
34
- attr_reader :states
35
78
  def state name, &block
36
79
  @states ||= {}
37
80
  @states[name] ||= block if block_given?
81
+ define_method name do
82
+ state_for name
83
+ end unless instance_methods.include? name
38
84
  end
85
+
86
+ #
87
+ #
88
+ attr_reader :states
89
+
90
+ # Return a state for this name (and dialog instance).
91
+ #
39
92
  def state_for name, instance
40
- # Lazily wrap.
93
+ # Lazily wrap in State instance.
41
94
  #
42
95
  if states[name].respond_to?(:call)
43
96
  states[name] = State.new(name, instance, &states[name])
data/lib/james/dialogs.rb CHANGED
@@ -1,41 +1,19 @@
1
1
  module James
2
2
 
3
- # Registers dialogs and connects their states.
3
+ # Bundles a bunch of dialogs.
4
4
  #
5
5
  class Dialogs
6
6
 
7
- attr_reader :initial
7
+ attr_reader :dialogs
8
8
 
9
- def initialize
10
- @initial = State.new :__initial_plugin_state__, nil
9
+ def initialize *dialogs
10
+ @dialogs = dialogs
11
11
  end
12
12
 
13
- # Generate the graph for the dialogs.
14
13
  #
15
- # Hooks up the entry phrases of the dialog
16
- # into the main dialog.
17
14
  #
18
- # It raises if the hook phrase of a dialog
19
- # is already used.
20
- #
21
- def << dialog
22
- resolved_entries = {}
23
-
24
- dialog.entries.each do |(phrases, state)|
25
- resolved_entries[phrases] = state.respond_to?(:phrases) ? state : dialog.state_for(state)
26
- end
27
-
28
- # Hook the dialog into the initial state.
29
- #
30
- initial.hear resolved_entries
31
- end
32
-
33
- # Get the visitor.
34
- #
35
- # Initialized on the initial state.
36
- #
37
- def visitor
38
- @visitor ||= Visitor.new initial
15
+ def chain_to incoming_dialog
16
+ dialogs.each { |dialog| incoming_dialog << dialog }
39
17
  end
40
18
 
41
19
  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,92 @@
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
+ current.__transition__ &state_or_lambda # Don't transition.
50
+ else
51
+ self.current = state_or_lambda
52
+ end
53
+ end
54
+
55
+ #
56
+ #
57
+ def check
58
+ yield("Whoops. That led nowhere. Perhaps you didn't define the target state?") unless self.current
59
+ end
60
+
61
+ # Returns falsy if it stays the same.
62
+ #
63
+ def process phrase, &block
64
+ exit_text = exit &block
65
+ last_context = current.context
66
+ transition phrase
67
+ check &block
68
+ into_text = enter &block
69
+ last_context != current.context
70
+ end
71
+
72
+ #
73
+ #
74
+ def hears? phrase
75
+ expects.include? phrase
76
+ end
77
+
78
+ # Does the current state allow penetration into another dialog?
79
+ #
80
+ def chainable?
81
+ current.chainable?
82
+ end
83
+
84
+ def to_s
85
+ "#{self.class.name}(#{initial}, current: #{current})"
86
+ end
87
+
88
+ end
89
+
90
+ end
91
+
92
+ 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
@@ -43,26 +43,42 @@ module James
43
43
  #
44
44
  def hear transitions
45
45
  transitions = { transitions => name } unless transitions.respond_to?(:to_hash)
46
- @transitions.merge! expand(transitions)
46
+ @transitions = expand(transitions).merge @transitions
47
47
  end
48
48
 
49
- # Execute this block when entering this state.
49
+ # Execute this block or say the text when entering this state.
50
50
  #
51
- def into &block
52
- @into_block = block
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__)
53
59
  end
54
60
 
55
- # Execute this block when exiting this state.
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)}" }
56
66
  #
57
- def exit &block
58
- @exit_block = block
67
+ def exit text = nil, &block
68
+ @exit_block = block ||
69
+ text && lambda { text } ||
70
+ raise_no_text_or_block(__method__)
59
71
  end
60
72
 
61
- # By default, a state is not chainable.
73
+ # Chain the given dialog to this state.
62
74
  #
63
- def chainable?
64
- !!@chainable
75
+ def << dialog
76
+ dialog.chain_to self
65
77
  end
78
+
79
+ # A chainable state is a state from which other
80
+ # Dialogs can be reached.
81
+ #
66
82
  def chainable
67
83
  @chainable = true
68
84
  end
@@ -70,7 +86,7 @@ module James
70
86
  # Description of self using name and transitions.
71
87
  #
72
88
  def to_s
73
- "#{self.class.name}(#{name}, #{context}, #{transitions})"
89
+ "#{self.class.name}(#{name}, #{context}, #{@transitions})"
74
90
  end
75
91
 
76
92
  # The naughty privates of this class.
@@ -90,6 +106,18 @@ module James
90
106
  results
91
107
  end
92
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
+
93
121
  end
94
122
 
95
123
  end