activeai 0.1.1 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: deea25fb847c585d5722d3ca53c369dd0af13864615fdc82013db1f70fa17e8e
4
- data.tar.gz: 0c6612d1caaff492e04862947a7d3a1b3aeebb747068be7e0a4dc18b8c6faaf0
3
+ metadata.gz: a327faafb1ed168dfbbfce2c3f5beed3b6090c8ed5c95d24913d988e39227143
4
+ data.tar.gz: f3bb5b04eabc7838193f099e649f5dbe48ae3f151537b28469d1b6738848fb00
5
5
  SHA512:
6
- metadata.gz: d540bd977bddbb80203bd79855bc62938981c073a0b787d7d8f0fb51db7ec20496e32b1f094b0626dce6d260efab368749aa11f69d25feefec36c09b9c5f4885
7
- data.tar.gz: 4de73f333f06560b445b453ec47520f759fd6c43da4fd56f8d03ed16b330c5ece5ff9b0d307ea6b9c4d0180061f24c2e3118545916a4c7c9c9bb22e36c09d4e4
6
+ metadata.gz: c3530fdce13cab64414b9b921a49672cd2dae8c170e6f50af5486fe30b33055403a1ef37b6a8c2e3172178ff61ac8c4d26292a3708877111d329cecb0dc59a72
7
+ data.tar.gz: 9dc7550488f4dab988b11719a065f0e7fd0c17afa2712c6670b97e8300969fb9d218ac47e2939d295134e5313594f00f45e8838c34c5d9fb178237e3292adcce
data/README.md CHANGED
@@ -39,6 +39,12 @@ puts result
39
39
  #=> 'tomatoes, jam'
40
40
  ```
41
41
 
42
+ ### Behavior: WriteFunctionCall
43
+
44
+ #### TODO
45
+
46
+ This lets you use `code-davinci-002` or `code-cushman-001` to run logic. The router uses this internally. Supply a list of example pairs that are a "description" and "code", and then you can complete another one.
47
+
42
48
  ### TODO: with other patterns
43
49
 
44
50
  ### TODO: auto-detected behavior pattern from config
@@ -47,29 +53,34 @@ puts result
47
53
 
48
54
  **This is the fun part!**
49
55
 
56
+ Suppose you have the following files:
57
+
50
58
  ### config/routes/bank.yml
51
59
 
52
60
  ```
53
61
  instruction:
54
- For a given Match request, choose where to send it via the "To" field and choose the params that fit best.
55
- If nothing matches, the "To" field should be None.
62
+ For a given Match request, choose where to send it via the "Route" field and choose the params that fit best.
63
+ If nothing matches, the "Route" field should be None.
56
64
  examples:
57
65
  - Match: Check the weather
58
- To: none
66
+ Route: none
59
67
  - Match: Send R100 to Jomiro
60
- To: bank#transfer_money
68
+ Route: bank#transfer_money
61
69
  Params: { beneficiaryId: 12345, amount: 100.0 }
62
70
  - Match: Pay Mom R245 for groceries
63
- To: bank#transfer_money
71
+ Route: bank#transfer_money
64
72
  Params: { beneficiaryId: 98765, amount: 245.0, reference: "Groceries <3" }
65
73
  - Match: What's my bank balance?
66
- To: bank#check_balance
74
+ Route: bank#check_balance
67
75
  ```
68
76
 
69
77
  ### controllers/bank_controller.rb
70
78
 
71
79
  ```
72
80
  class BankController < ActiveAI::Controller
81
+ auto_load_routing # loads routing config from config/routes/bank.yml
82
+ load_routing(config) # alternatively, loads routing config from a hash
83
+
73
84
  def check_balance
74
85
  # Make an API request to GET bank.com/balance and return some useful data
75
86
  end
@@ -79,3 +90,50 @@ class BankController < ActiveAI::Controller
79
90
  end
80
91
  end
81
92
  ```
93
+
94
+ ### How to use it
95
+
96
+ #### Running a controller directly
97
+
98
+ Using the routing yml file and an LLM, the controller will turn any text request into an action to run, with parameters to supply, and then execute it.
99
+
100
+ ```ruby
101
+ controller = BankController.new
102
+ controller.call("Pay Mom R127 for groceries")
103
+ # => responds with the result of an action that ran with params
104
+ ```
105
+
106
+ #### Routing an unknown request with multiple controllers
107
+
108
+ It's possible to instantiate an `ActiveAI::Router`, load up the examples from multiple controllers, and then have it handle many types of requests. It does this in a similar way to how the controller uses an LLM to map to action and params, but it concatenates all controller routing examples and strips out the parameter preparation step for efficiency, since the controller handles this.
109
+
110
+ ```ruby
111
+ router = ActiveAI::Router.new
112
+
113
+ # load all auto-detected routes:
114
+ router.auto_load_routing(Rails.root.join('config','routes')) # loads all .yml files as controller examples
115
+
116
+ # or, load config via path or manually from a config hash:
117
+ router.add_controller_routing_from_path(Rails.root.join("config", "routes", "bank.yml"))
118
+ slack_routing = YAML::load(File.read(Rails.root.join("config", "routes", "slack.yml"))
119
+ router.add_controller_routing(slack_routing)
120
+ ```
121
+
122
+ Once the routes are loaded, requests will be passed to a matched controller, if any matches. You can match and run requests like this:
123
+
124
+ ```ruby
125
+ router.call("Send a Slack message saying 'Hey everyone!") # returns the successful action
126
+ router.call("Transfer R5,000 to savings") # returns the successful action
127
+ router.call("Visit grandma") # returns nil
128
+ ```
129
+
130
+ Or if you just want to find the controller:
131
+
132
+ ```ruby
133
+ router.find_controller("Transfer money out of savings")
134
+ # => BankController
135
+ ```
136
+
137
+ # Please help make this better!
138
+
139
+ This is an experiment to push the boundaries of "AI as compute" and it would be awesome to have more eager explorers to play with!
data/SESSIONS.md CHANGED
@@ -3,15 +3,21 @@
3
3
  ## What I did
4
4
 
5
5
  - Fix active_ai/controller.rb to reflect on the correct detected path name
6
- - Ran a full test with thinkspawn creating DALLE art - it's really cool!
7
-
8
- TODO
9
-
10
- - publish v0.1.1 that actually works!
6
+ - Ran a full test with thinkspawn creating DALLE art and sending Slack messages - it's really cool!
7
+ - Released v0.1.1!!
11
8
 
12
9
  ## What I learned
13
10
 
14
11
  - Gonna need a master router and make the sub-routers just about param prep
12
+ - Curie doesn't do great with routing many controllers. I had to rename e.g. slack.notify to slack.send_message because that seemed to be Curie's default.. DaVinci works better but also some issues i think
13
+
14
+ ## What I could do next
15
+
16
+ - add whisper as a neural network via replicate.com
17
+ - add embeddings as a concept - not sure what/how yet
18
+ - make better examples for routing, it's a bit iffy right now
19
+ - add a better way in code to test controllers test cases etc.
20
+ - test out if the code models work better than the text ones
15
21
 
16
22
  # 30 December 2022
17
23
 
@@ -20,7 +26,7 @@ TODO
20
26
  - Built a basic gem!
21
27
  - Added behaviors, specifically for structured trained examples
22
28
  - Added rails controllers and routers which use structured trained examples
23
- - Added a cool prototype conversation structure, and a cool idea on how to make operators ponder!
29
+ - Added a cool prototype conversation/chat structure, and a cool idea on how to make operators ponder!
24
30
 
25
31
  ## What I learned
26
32
 
@@ -36,5 +42,4 @@ TODO
36
42
  - Publish v0.1.1
37
43
  - Update the configuration.rb mechanic a bit
38
44
  - Load up all OpenAI's examples as instances of one of a couple of behavior types
39
- - Build a chat behavior
40
45
  - Add session contexts to the router registration so it's flexible
@@ -20,7 +20,7 @@ class ActiveAI::Behavior::LLM::Conversation < ActiveAI::Behavior::LLM
20
20
  # TODO use the label key they provide in the yml file
21
21
  end,
22
22
  "Conversation:\n" + @state['conversation']
23
- ].join(SEPARATOR)
23
+ ].join(LINE_SEPARATOR)
24
24
  end
25
25
 
26
26
  def add(speaker, message)
@@ -15,12 +15,12 @@ class ActiveAI::Behavior::LLM::FollowStructuredExamples < ActiveAI::Behavior::LL
15
15
  example.map do |key, value|
16
16
  "#{key}: #{value}"
17
17
  end.join("\n")
18
- end.join(SEPARATOR)
19
- ].join(SEPARATOR)
18
+ end.join(LINE_SEPARATOR)
19
+ ].join(LINE_SEPARATOR)
20
20
  end
21
21
 
22
22
  def call(input={}, extract: []) # TODO cool splat stuff?
23
- prompt = base_prompt + SEPARATOR
23
+ prompt = base_prompt + LINE_SEPARATOR
24
24
 
25
25
  prompt += input.map do |key, value|
26
26
  "#{key}: #{value}"
@@ -0,0 +1,35 @@
1
+ class ActiveAI::Behavior::LLM::WriteFunctionCall < ActiveAI::Behavior::LLM
2
+ def initialize(llm, state)
3
+ super(llm)
4
+ @state = state
5
+ # TODO raise errors if not expected thingies available in the config
6
+ end
7
+
8
+ def base_prompt
9
+ @state[:examples].map do |example|
10
+ [
11
+ "// #{example[:description]}",
12
+ example[:code]
13
+ ].join("\n")
14
+ end.join("\n\n") + "\n\n"
15
+ end
16
+
17
+ def call(comment)
18
+ prompt = base_prompt + "\n\n"
19
+ prompt += "//#{comment}\n"
20
+ complete_result = complete(prompt, stop: "\n")
21
+
22
+ # TODO stop \n works for the router but not for other stuff, later
23
+
24
+ completion = complete_result['choices'][0]['text']
25
+
26
+ matcher = /(.*)\((.*)\)/
27
+ matches = matcher.match(completion)
28
+
29
+ return {
30
+ text: completion.strip,
31
+ path: matches[1],
32
+ params: matches[2].presence && JSON.parse(matches[2])
33
+ }
34
+ end
35
+ end
@@ -7,7 +7,7 @@ class ActiveAI::Behavior::LLM < ActiveAI::Behavior::Base
7
7
  @llm.complete(prompt: prompt, stop: stop)
8
8
  end
9
9
 
10
- SEPARATOR = "\n\n###\n\n"
10
+ LINE_SEPARATOR = "\n\n###\n\n"
11
11
 
12
12
  def extract_keys(completion, extract)
13
13
  matcher_string = extract.map{ |key| "#{key}:(.*)" }.join
@@ -25,5 +25,6 @@ class ActiveAI::Behavior::LLM < ActiveAI::Behavior::Base
25
25
  end
26
26
 
27
27
  require_relative "llm/conversation"
28
- require_relative "llm/unstructured"
29
28
  require_relative "llm/follow_structured_examples"
29
+ require_relative "llm/unstructured"
30
+ require_relative "llm/write_function_call"
@@ -1,19 +1,59 @@
1
1
  class ActiveAI::Controller
2
- # auto-loads a router from current file when inherited
3
- class_attribute :router
4
2
 
5
- def self.inherited(base)
6
- # TODO I'm sure there's a proper way to do this but it works for now
7
- routes_path = Rails.root.join('config', 'routes', base.to_s.underscore.gsub('_controller', '.yml'))
3
+ class_attribute :routing_behavior
4
+
5
+ def self.auto_load_routing
6
+ routes_path = Rails.root.join('config', 'routes', self.to_s.underscore.gsub('_controller', '.yml'))
8
7
  routes_config = YAML::load(File.read(routes_path))
9
- # convert routes_config params into JSON?
10
- router = ActiveAI::Router.new(routes_config, controller: base)
8
+ self.load_routing(routes_config)
9
+ end
10
+
11
+ def self.load_routing(routes_config)
12
+ @llm = ActiveAI::NeuralNetwork::GPT3.new(ActiveAI.config[:gpt3_token], model: 'code-davinci-002', temperature: 0.2)
13
+
14
+ examples = ActiveAI.route_examples_to_function_call_examples(routes_config['examples'])
15
+ self.routing_behavior = ActiveAI::Behavior::LLM::WriteFunctionCall.new(@llm, { examples: examples })
11
16
  end
12
17
 
13
18
  attr_accessor :params
14
19
 
15
- def call(method_name, params)
20
+ def prepare_action(request)
21
+ # samples to parse:
22
+ # plugins.slack.send_message({ \"channel\": \"#general\", \"text\": \"Hi\" })
23
+ # unmatched()
24
+
25
+ function = self.class.routing_behavior.call(request)
26
+ *controller_path, action_name = function[:path].split(".")
27
+ controller_name = controller_path.join("/").presence
28
+
29
+ # TODO verify it's the right controller and the action name exists and it's not a reserved / internal thing
30
+
31
+ if controller_name.present?
32
+ return {
33
+ action: action_name,
34
+ params: function[:params]
35
+ }
36
+ else
37
+ return nil
38
+ end
39
+ end
40
+
41
+ def call(request)
42
+ mapped_request = prepare_action(request)
43
+
44
+ if mapped_request
45
+ return run_action(mapped_request[:action], mapped_request[:params])
46
+ else
47
+ return nil
48
+ end
49
+ end
50
+
51
+ def run_action(action_name, params)
16
52
  @params = params
17
- send(method_name)
53
+ response = send(action_name)
54
+ # handle response somehow, or do we just dump JSON back?
18
55
  end
56
+
57
+ # surely this is where the magic prep loading and unloading happens?
58
+ # i.e. the params deal with this
19
59
  end
@@ -1,74 +1,79 @@
1
1
  class ActiveAI::Router
2
- INSTRUCTION_ALL = 'For a given Match request, choose where to send it via the "To" field. If nothing matches, the "To" field should be None.'
3
- UNMATCHED_ALL = { 'Match' => 'Create a NASA space program', 'To' => 'None' }
4
- INSTRUCTION_INSTANCE = 'For a given Match request, choose where to send it via the "To" field. Also choose the params that fit best. If nothing matches, the "To" field should be None.'
2
+ INSTRUCTION = 'For a given Match request, choose where to send it via the "Route" field. If nothing matches, the "Route" field should be None.'
3
+ UNMATCHED = { 'Match' => 'Create a NASA space program', 'Route' => 'None' }
5
4
 
6
- # TODO could load a "session" or "context" for current user which handles router registration and stuff
7
- # keeps it flexi for thinkspawn while not breaking things on this layer
8
-
9
- class_attribute :routers
10
- self.routers = []
5
+ def initialize
6
+ @routings = []
7
+ @llm = ActiveAI::NeuralNetwork::GPT3.new(ActiveAI.config[:gpt3_token], model: 'code-davinci-002', temperature: 0.2)
8
+ end
11
9
 
12
- def self.application_router # TODO use this
13
- new({
14
- 'instruction' => INSTRUCTION_ALL,
15
- 'examples' => routers.map do |router|
16
- router.config['examples'].select do |example|
17
- example['To'] != 'None'
18
- end.map do |example|
19
- example.slice('Match', 'To')
20
- end << UNMATCHED_ALL
21
- end.flatten
22
- })
10
+ def add_controller_routing(routing)
11
+ @routings << routing
23
12
  end
24
13
 
25
- def self.call_all(request)
26
- # this shouldn't be the thing, something just decides if it matches or not
27
- # we actually ask a master router where to go, and then ask the specific router what the params are
28
- # -- might hit the token limit though, so why not just do each one? less web requests? but infinite scale
14
+ def add_controller_routing_from_path(path)
15
+ routing = YAML::load(File.read(path))
16
+ add_controller_routing(routing)
17
+ end
29
18
 
30
- routers.each do |router|
31
- response = router.call(request)
32
- return response if response
19
+ def auto_load_routing(folder)
20
+ paths = Dir[folder.join("**", "*.yml")]
21
+ paths.each do |path|
22
+ add_controller_routing_from_path(path)
33
23
  end
34
24
  end
35
25
 
36
- attr_accessor :config
26
+ def behavior
27
+ raw_examples = [UNMATCHED] + @routings.map do |routing|
28
+ routing['examples'].reject do |example|
29
+ example['Route'] == 'None'
30
+ end.map do |example|
31
+ example.slice('Match', 'Route')
32
+ end
33
+ end.flatten
34
+ examples = ActiveAI.route_examples_to_function_call_examples(raw_examples)
35
+
36
+ ActiveAI::Behavior::LLM::WriteFunctionCall.new(@llm, { examples: examples })
37
+ end
38
+
39
+ # def behavior_via_structured_examples
40
+ # config = {
41
+ # 'instruction' => INSTRUCTION,
42
+ # 'examples' => [UNMATCHED] + @routings.map do |routing|
43
+ # routing['examples'].reject do |example|
44
+ # example['Route'] == 'None'
45
+ # end.map do |example|
46
+ # example.slice('Match', 'Route')
47
+ # end
48
+ # end.flatten
49
+ # }
37
50
 
38
- def initialize(config, controller: nil)
39
- self.class.routers ||= []
51
+ # ActiveAI::Behavior::LLM::FollowStructuredExamples.new(@llm, config)
52
+ # end
40
53
 
41
- @config = config
42
- @config['instruction'] ||= INSTRUCTION_INSTANCE
54
+ def find_controller(request)
55
+ function = behavior.call(request) # TODO maybe the behavior should return function and params as well. seems right
56
+
57
+ *controller_path, action_name = function[:path].split(".")
58
+ controller_name = controller_path.join("/").presence
43
59
 
44
- llm = ActiveAI::NeuralNetwork::GPT3.new(ActiveAI.config[:gpt3_token], model: 'text-curie-001')
45
- @behavior = ActiveAI::Behavior::LLM::FollowStructuredExamples.new(llm, config)
60
+ # TODO verify it's the right controller and the action name exists and it's not a reserved / internal thing
46
61
 
47
- if controller
48
- self.class.routers << self
49
- @controller = controller.new
62
+ if controller_name.blank? || action_name.blank? || action_name == 'unmatched'
63
+ return nil
64
+ else
65
+ return (controller_name + "_controller").classify.constantize
66
+ # TODO need protection (somewhere) from using controllers that aren't allowed
67
+ # maybe router has a whitelist? since we're taking user input
68
+ # idk problem for later not now
50
69
  end
51
70
  end
52
71
 
53
72
  def call(request)
54
- routing = @behavior.call({ 'Request' => request }, extract: %W[To Params]) # TODO might not have params returned, will break?
55
-
56
- controller_name, method_name = routing['To'].split('#')
57
-
58
- if [controller_name, method_name].any?(&:blank?)
59
- # unmatched
60
- nil
73
+ if controller = find_controller(request)
74
+ controller.new.call(request)
61
75
  else
62
- params = JSON.parse(routing['Params']) # TODO cast as JSON earlier? e.g. in config of the behavior?
63
- puts "Calling #{method_name} with params: #{params}." # but only if matched
64
-
65
- if @controller.is_a?(ActiveAI::Controller)
66
- return @controller.call(method_name, params)
67
- else
68
- # it could be a dynamic user-generated script or something to call out to somehow
69
- # TODO later later
70
- return true
71
- end
76
+ return nil
72
77
  end
73
78
  end
74
79
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveAI
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
data/lib/activeai.rb CHANGED
@@ -16,5 +16,17 @@ module ActiveAI
16
16
  }
17
17
  end
18
18
 
19
+ def self.route_examples_to_function_call_examples(examples)
20
+ examples.map do |example|
21
+ function = example['Route'].gsub('/','.').gsub('#','.')
22
+ function = "unmatched" if function == "None"
23
+
24
+ {
25
+ description: example['Match'],
26
+ code: "#{function}(#{example['Params']&.strip})"
27
+ }
28
+ end
29
+ end
30
+
19
31
  end
20
32
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activeai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - jeriko
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-12-31 00:00:00.000000000 Z
11
+ date: 2023-01-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -64,6 +64,7 @@ files:
64
64
  - lib/activeai/behavior/llm/conversation.rb
65
65
  - lib/activeai/behavior/llm/follow_structured_examples.rb
66
66
  - lib/activeai/behavior/llm/unstructured.rb
67
+ - lib/activeai/behavior/llm/write_function_call.rb
67
68
  - lib/activeai/configuration.rb
68
69
  - lib/activeai/controller.rb
69
70
  - lib/activeai/neural_network.rb