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

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/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