sublayer 0.0.8 → 0.1.0.pre.alpha

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c93987f0281e2fa58ba4373a7eb3be0e93d6053dd211379ee0ec1f3e26dedf45
4
- data.tar.gz: 0b8473c287e9c601ab91bb0965521a69c71bd835576c5f4f56a3cdadc3fac2c3
3
+ metadata.gz: 5a723acaec8ce50c64470d067dc29769fb8efe9415e4a867507272ef8961e687
4
+ data.tar.gz: 76508d4f490e5d9601d3827e35191b20ba7887e2086696bb610567e0f49639bd
5
5
  SHA512:
6
- metadata.gz: 40e50ae299dfd876158756ed97e71d330bbc05ad24fad400308e22af81ea16c09213d46b54deff1bb11de1fe8ab164a60fb094e9f857db383584fca5348f4d4b
7
- data.tar.gz: a66a4a3271bf886dc20cebed86d414c63046dade251bd05fa4f3e6f7a4898671ec323df45ea0bafefbe63b13c4667e9da33bc243b677a5f280c3b278501b51f5
6
+ metadata.gz: 983e31c6d8b85a6116accf2b7eb395b257e79de967e79f4046ddfacf046522bc274335e931e8665f67864704fd2af7b331cd93cc294f50d9533f44bf99d94f39
7
+ data.tar.gz: 3a18adf3d93a074897855874780f7ae0205ed5de3541923f5711a238ab0823fadedb3a0a82fe32c87cbca5da918656d0fa9d982bb2a3ef1651a5a606f5647d81
data/README.md CHANGED
@@ -14,10 +14,10 @@ for new features and bug fixes.
14
14
 
15
15
  To maintain stability in your application, we recommend pinning the version of
16
16
  Sublayer in your Gemfile to a specific minor version. For example, to pin to
17
- version 0.0.x, you would add the following line to your Gemfile:
17
+ version 0.1.x, you would add the following line to your Gemfile:
18
18
 
19
19
  ```ruby
20
- gem 'sublayer', '~> 0.0'
20
+ gem 'sublayer', '~> 0.1'
21
21
  ```
22
22
 
23
23
  ## Installation
@@ -29,7 +29,7 @@ Install the gem by running the following commands:
29
29
  Or add this line to your application's Gemfile:
30
30
 
31
31
  ```ruby
32
- gem 'sublayer', '~> 0.0'
32
+ gem 'sublayer', '~> 0.1'
33
33
  ```
34
34
 
35
35
  ## Choose your AI Model
@@ -73,72 +73,6 @@ Sublayer.configuration.ai_provider = Sublayer::Providers::Claude
73
73
  Sublayer.configuration.ai_model ="claude-3-opus-20240229"
74
74
  ```
75
75
 
76
- ### Groq
77
-
78
- Expects you to have a Groq API key set in the `GROQ_API_KEY` environment variable.
79
-
80
- Visit [Groq Console](https://console.groq.com/) to get an API key.
81
-
82
- Usage:
83
- ```ruby
84
- Sublayer.configuration.ai_provider = Sublayer::Providers::Groq
85
- Sublayer.configuration.ai_model = "mixtral-8x7b-32768"
86
- ```
87
-
88
- ### Local
89
-
90
- If you've never run a local model before see the [Local Model Quickstart](#local-model-quickstart) below. Know that local models take several GB of space.
91
-
92
- The model you use must have the ChatML formatted v1/chat/completions endpoint to work with sublayer (many models do by default)
93
-
94
- Usage:
95
-
96
- Run your local model on http://localhost:8080 and then set:
97
- ```ruby
98
- Sublayer.configuration.ai_provider = Sublayer::Providers::Local
99
- Sublayer.configuration.ai_model = "LLaMA_CPP"
100
- ```
101
-
102
- #### Local Model Quickstart:
103
-
104
- Instructions to run a local model
105
-
106
- 1. Setting up Llamafile
107
-
108
- ```bash
109
- cd where/you/keep/your/projects
110
- git clone git@github.com:Mozilla-Ocho/llamafile.git
111
- cd llamafile
112
- ```
113
-
114
- Download: https://cosmo.zip/pub/cosmos/bin/make (windows users need this too: https://justine.lol/cosmo3/)
115
-
116
- ```bash
117
- # within llamafile directory
118
- chmod +x path/to/the/downloaded/make
119
- path/to/the/downloaded/make -j8
120
- sudo path/to/the/downloaded/make install PREFIX=/usr/local
121
- ```
122
- You can now run llamfile
123
-
124
- 2. Downloading Model
125
-
126
- click [here](https://huggingface.co/NousResearch/Hermes-2-Pro-Mistral-7B-GGUF/resolve/main/Hermes-2-Pro-Mistral-7B.Q5_K_M.gguf?download=true) to download Mistral_7b.Q5_K_M (5.13 GB)
127
-
128
- 3. Running Llamafile with a model
129
-
130
- ```bash
131
- llamafile -ngl 9999 -m path/to/the/downloaded/Hermes-2-Pro-Mistral-7B.Q5_K_M.gguf --host 0.0.0.0 -c 4096
132
- ```
133
-
134
- You are now running a local model on http://localhost:8080
135
-
136
- #### Recommended Settings for Apple M1 users:
137
- ```bash
138
- llamafile -ngl 9999 -m Hermes-2-Pro-Mistral-7B.Q5_K_M.gguf --host 0.0.0.0 --nobrowser -c 2048 --gpu APPLE -t 12
139
- ```
140
- run `sysctl -n hw.logicalcpu` to see what number to give the `-t` threads option
141
-
142
76
  ## Concepts
143
77
 
144
78
  ### Generators
@@ -203,11 +137,3 @@ base for generating new code.
203
137
 
204
138
  - [Clag](https://github.com/sublayerapp/clag) - A ruby gem that generates
205
139
  command line commands from a simple description right in your terminal.
206
-
207
- ## Development
208
-
209
- TBD
210
-
211
- ## Contributing
212
-
213
- TBD
@@ -2,41 +2,15 @@ module Sublayer
2
2
  module Components
3
3
  module OutputAdapters
4
4
  class SingleString
5
- attr_reader :name
5
+ attr_reader :name, :description
6
6
 
7
7
  def initialize(options)
8
8
  @name = options[:name]
9
9
  @description = options[:description]
10
10
  end
11
11
 
12
- def to_hash
13
- {
14
- name: @name,
15
- description: @description,
16
- parameters: {
17
- type: "object",
18
- properties: {
19
- @name => {
20
- type: "string",
21
- description: @description
22
- }
23
- }
24
- }
25
- }
26
- end
27
-
28
- def to_xml
29
- <<-XML
30
- <tool_description>
31
- <tool_name>#{@name}</tool_name>
32
- <tool_description>#{@description}</tool_description>
33
- <parameters>
34
- <name>#{@name}</name>
35
- <type>string</type>
36
- <description>#{@description}</description>
37
- </parameters>
38
- </tool_description>
39
- XML
12
+ def properties
13
+ [OpenStruct.new(name: @name, type: 'string', description: @description, required: true)]
40
14
  end
41
15
  end
42
16
  end
@@ -0,0 +1,30 @@
1
+ module Sublayer
2
+ module Components
3
+ module OutputAdapters
4
+ class StringSelectionFromList
5
+ attr_reader :name, :description, :options
6
+
7
+ def initialize(options)
8
+ @name = options[:name]
9
+ @description = options[:description]
10
+ @list = options[:options]
11
+ end
12
+
13
+ def properties
14
+ [OpenStruct.new(name: @name, type: 'string', description: @description, required: true, enum: @list)]
15
+ end
16
+
17
+ def load_instance_data(generator)
18
+ case @list
19
+ when Proc
20
+ @list = generator.instance_exec(&@list)
21
+ when Symbol
22
+ @list = generator.send(@list)
23
+ else
24
+ @list
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -9,6 +9,7 @@ module Sublayer
9
9
  end
10
10
 
11
11
  def generate
12
+ self.class::OUTPUT_ADAPTER.load_instance_data(self) if self.class::OUTPUT_ADAPTER.respond_to?(:load_instance_data)
12
13
  @results = Sublayer.configuration.ai_provider.call(prompt: prompt, output_adapter: self.class::OUTPUT_ADAPTER)
13
14
  end
14
15
  end
@@ -0,0 +1,29 @@
1
+ class RouteSelectionFromUserIntentGenerator < Sublayer::Generators::Base
2
+ llm_output_adapter type: :string_selection_from_list,
3
+ name: "route",
4
+ description: "A route selected from the list",
5
+ options: :available_routes
6
+
7
+ def initialize(user_intent:)
8
+ @user_intent = user_intent
9
+ end
10
+
11
+ def generate
12
+ super
13
+ end
14
+
15
+ def available_routes
16
+ ["GET /", "GET /users", "GET /users/:id", "POST /users", "PUT /users/:id", "DELETE /users/:id"]
17
+ end
18
+
19
+ def prompt
20
+ <<-PROMPT
21
+ You are skilled at selecting routes based on user intent.
22
+
23
+ Your task is to choose a route based on the following intent:
24
+
25
+ The user's intent is:
26
+ #{@user_intent}
27
+ PROMPT
28
+ end
29
+ end
@@ -0,0 +1,26 @@
1
+ class SentimentFromTextGenerator < Sublayer::Generators::Base
2
+ llm_output_adapter type: :string_selection_from_list,
3
+ name: "sentiment_value",
4
+ description: "A sentiment value from the list",
5
+ options: -> { @sentiment_options }
6
+
7
+ def initialize(text:, sentiment_options:)
8
+ @text = text
9
+ @sentiment_options = sentiment_options
10
+ end
11
+
12
+ def generate
13
+ super
14
+ end
15
+
16
+ def prompt
17
+ <<-PROMPT
18
+ You are an expert at determining sentiment from text.
19
+
20
+ You are tasked with analyzing the following text and determining its sentiment value.
21
+
22
+ The text is:
23
+ #{@text}
24
+ PROMPT
25
+ end
26
+ end
@@ -5,49 +5,49 @@ module Sublayer
5
5
  module Providers
6
6
  class Claude
7
7
  def self.call(prompt:, output_adapter:)
8
- system_prompt = <<-PROMPT
9
- In this environment you have access to a set of tools you can use to answer the user's question.
10
-
11
- You may call them like this:
12
- <function_calls>
13
- <invoke>
14
- <tool_name>$TOOL_NAME</tool_name>
15
- <parameters>
16
- <$PARAMETER_NAME>$PARAMETER_VALUE</$PARAMETER_NAME>
17
- ...
18
- </parameters>
19
- </invoke>
20
- </function_calls>
21
-
22
- Here are the tools available:
23
- <tools>
24
- #{output_adapter.to_xml}
25
- </tools>
26
-
27
- Respond only with valid xml. The entire response should be wrapped in a <response> tag. Any additional information not inside a tool call should go in a <scratch> tag.
28
- PROMPT
29
-
30
8
  response = HTTParty.post(
31
9
  "https://api.anthropic.com/v1/messages",
32
10
  headers: {
33
11
  "x-api-key": ENV.fetch("ANTHROPIC_API_KEY"),
34
12
  "anthropic-version": "2023-06-01",
35
- "content-type": "application/json"
13
+ "content-type": "application/json",
14
+ "anthropic-beta": "tools-2024-04-04"
36
15
  },
37
16
  body: {
38
17
  model: Sublayer.configuration.ai_model,
39
18
  max_tokens: 4096,
40
- system: system_prompt,
41
- messages: [ { "role": "user", "content": prompt }]
19
+ tools: [
20
+ {
21
+ name: output_adapter.name,
22
+ description: output_adapter.description,
23
+ input_schema: {
24
+ type: "object",
25
+ properties: format_properties(output_adapter),
26
+ required: output_adapter.properties.select(&:required).map(&:name)
27
+ }
28
+ }
29
+ ],
30
+ messages: [{ "role": "user", "content": prompt }]
42
31
  }.to_json
43
32
  )
44
33
  raise "Error generating with Claude, error: #{response.body}" unless response.code == 200
45
34
 
46
- text_containing_xml = JSON.parse(response.body).dig("content", 0, "text")
47
- function_output = Nokogiri::HTML.parse(text_containing_xml.match(/\<#{output_adapter.name}\>(.*?)\<\/#{output_adapter.name}\>/m)[1]).text
35
+ function_input = JSON.parse(response.body).dig("content").find {|content| content['type'] == 'tool_use'}.dig("input")
36
+ function_input[output_adapter.name]
37
+ end
38
+
39
+ private
40
+ def self.format_properties(output_adapter)
41
+ output_adapter.properties.each_with_object({}) do |property, hash|
42
+ hash[property.name] = {
43
+ type: property.type,
44
+ description: property.description
45
+ }
48
46
 
49
- raise "Claude did not format response, error: #{response.body}" unless function_output
50
- return function_output
47
+ if property.enum
48
+ hash[property.name][:enum] = property.enum
49
+ end
50
+ end
51
51
  end
52
52
  end
53
53
  end
@@ -21,9 +21,13 @@ module Sublayer
21
21
 
22
22
  Here are the tools available:
23
23
  <tools>
24
- <tool>
25
- #{output_adapter.to_xml}
26
- </tool>
24
+ <tool_description>
25
+ <tool_name>#{output_adapter.name}</tool_name>
26
+ <tool_description>#{output_adapter.description}</tool_description>
27
+ <parameters>
28
+ #{format_properties(output_adapter)}
29
+ </parameters>
30
+ </tool_description>
27
31
  </tools>
28
32
 
29
33
  Respond only with valid xml.
@@ -49,6 +53,17 @@ module Sublayer
49
53
  raise "Gemini did not format response, error: #{response.body}" unless tool_output
50
54
  return tool_output
51
55
  end
56
+
57
+ private
58
+ def self.format_properties(output_adapter)
59
+ output_adapter.properties.each_with_object("") do |property, xml|
60
+ xml << "<name>#{property.name}</name>"
61
+ xml << "<type>#{property.type}</type>"
62
+ xml << "<description>#{property.description}</description>"
63
+ xml << "<required>#{property.required}</required>"
64
+ xml << "<enum>#{property.enum}</enum>" if property.enum
65
+ end
66
+ end
52
67
  end
53
68
  end
54
69
  end
@@ -16,18 +16,44 @@ module Sublayer
16
16
  "content": prompt
17
17
  }
18
18
  ],
19
- function_call: { name: output_adapter.name },
20
- functions: [
21
- output_adapter.to_hash
19
+ tool_choice: { type: "function", function: { name: output_adapter.name }},
20
+ tools: [
21
+ {
22
+ type: "function",
23
+ function: {
24
+ name: output_adapter.name,
25
+ description: output_adapter.description,
26
+ parameters: {
27
+ type: "object",
28
+ properties: OpenAI.format_properties(output_adapter)
29
+ },
30
+ required: [output_adapter.properties.select(&:required).map(&:name)]
31
+ }
32
+ }
22
33
  ]
34
+
23
35
  })
24
36
 
25
37
  message = response.dig("choices", 0, "message")
26
- raise "No function called" unless message["function_call"]
27
38
 
28
- function_name = message.dig("function_call", output_adapter.name)
29
- args_from_llm = message.dig("function_call", "arguments")
30
- JSON.parse(args_from_llm)[output_adapter.name]
39
+ raise "No function called" unless message["tool_calls"].length > 0
40
+
41
+ function_body = message.dig("tool_calls", 0, "function", "arguments")
42
+ JSON.parse(function_body)[output_adapter.name]
43
+ end
44
+
45
+ private
46
+ def self.format_properties(output_adapter)
47
+ output_adapter.properties.each_with_object({}) do |property, hash|
48
+ hash[property.name] = {
49
+ type: property.type,
50
+ description: property.description
51
+ }
52
+
53
+ if property.enum
54
+ hash[property.name][:enum] = property.enum
55
+ end
56
+ end
31
57
  end
32
58
  end
33
59
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sublayer
4
- VERSION = "0.0.8"
4
+ VERSION = "0.1.0-alpha"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sublayer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.1.0.pre.alpha
5
5
  platform: ruby
6
6
  authors:
7
7
  - Scott Werner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-15 00:00:00.000000000 Z
11
+ date: 2024-05-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ruby-openai
@@ -180,15 +180,16 @@ files:
180
180
  - lib/sublayer/agents/base.rb
181
181
  - lib/sublayer/components/output_adapters.rb
182
182
  - lib/sublayer/components/output_adapters/single_string.rb
183
+ - lib/sublayer/components/output_adapters/string_selection_from_list.rb
183
184
  - lib/sublayer/generators/base.rb
184
185
  - lib/sublayer/generators/examples/code_from_blueprint_generator.rb
185
186
  - lib/sublayer/generators/examples/code_from_description_generator.rb
186
187
  - lib/sublayer/generators/examples/description_from_code_generator.rb
187
188
  - lib/sublayer/generators/examples/invalid_to_valid_json_generator.rb
189
+ - lib/sublayer/generators/examples/route_selection_from_user_intent_generator.rb
190
+ - lib/sublayer/generators/examples/sentiment_from_text_generator.rb
188
191
  - lib/sublayer/providers/claude.rb
189
192
  - lib/sublayer/providers/gemini.rb
190
- - lib/sublayer/providers/groq.rb
191
- - lib/sublayer/providers/local.rb
192
193
  - lib/sublayer/providers/open_ai.rb
193
194
  - lib/sublayer/tasks/base.rb
194
195
  - lib/sublayer/version.rb
@@ -212,9 +213,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
212
213
  version: 2.6.0
213
214
  required_rubygems_version: !ruby/object:Gem::Requirement
214
215
  requirements:
215
- - - ">="
216
+ - - ">"
216
217
  - !ruby/object:Gem::Version
217
- version: '0'
218
+ version: 1.3.1
218
219
  requirements: []
219
220
  rubygems_version: 3.3.26
220
221
  signing_key:
@@ -1,52 +0,0 @@
1
- # Sublayer.configuration.ai_provider = Sublayer::Providers::Groq
2
- # Sublayer.configuration.ai_model = "mixtral-8x7b-32768"
3
-
4
- module Sublayer
5
- module Providers
6
- class Groq
7
- def self.call(prompt:, output_adapter:)
8
- system_prompt = <<-PROMPT
9
- You have access to a set of tools to answer the prompt.
10
-
11
- You may call tools like this:
12
- <tool_calls>
13
- <tool_call>
14
- <tool_name>$TOOL_NAME</tool_name>
15
- <parameters>
16
- <#{output_adapter.name}>$VALUE</#{output_adapter.name}>
17
- ...
18
- </parameters>
19
- </tool_call>
20
- </tool_calls>
21
-
22
- Here are the tools available:
23
- <tools>
24
- #{output_adapter.to_xml}
25
- </tools>
26
-
27
- Respond only with valid xml.
28
- The entire response should be wrapped in a <response> tag.
29
- Your response should call a tool inside a <tool_calls> tag.
30
- PROMPT
31
-
32
- response = HTTParty.post(
33
- "https://api.groq.com/openai/v1/chat/completions",
34
- headers: {
35
- "Authorization": "Bearer #{ENV["GROQ_API_KEY"]}",
36
- "Content-Type": "application/json"
37
- },
38
- body: {
39
- "messages": [{"role": "user", "content": "#{system_prompt}\n#{prompt}"}],
40
- "model": Sublayer.configuration.ai_model
41
- }.to_json
42
- )
43
-
44
- text_containing_xml = response.dig("choices", 0, "message", "content")
45
- tool_output = Nokogiri::HTML.parse(text_containing_xml.match(/\<#{output_adapter.name}\>(.*?)\<\/#{output_adapter.name}\>/m)[1]).text
46
- raise "Groq did not format response correctly, error: #{response.body}" unless tool_output
47
-
48
- return tool_output
49
- end
50
- end
51
- end
52
- end
@@ -1,50 +0,0 @@
1
- # Sublayer.configuration.ai_provider = Sublayer::Providers::Local
2
- # Sublayer.configuration.ai_model = "LLaMA_CPP"
3
-
4
- module Sublayer
5
- module Providers
6
- class Local
7
- def self.call(prompt:, output_adapter:)
8
- system_prompt = <<-PROMPT
9
- You have access to a set of tools to respond to the prompt.
10
-
11
- You may call a tool with xml like this:
12
- <parameters>
13
- <#{output_adapter.name}>$VALUE</#{output_adapter.name}>
14
- ...
15
- </parameters>
16
-
17
- Here are descriptions of the available tools:
18
- <tools>
19
- <tool>
20
- #{output_adapter.to_xml}
21
- </tool>
22
- </tools>
23
-
24
- Respond only with valid xml.
25
- Your response should call a tool with xml inside a <parameters> tag.
26
- PROMPT
27
-
28
- response = HTTParty.post(
29
- "http://localhost:8080/v1/chat/completions",
30
- headers: {
31
- "Authorization": "Bearer no-key",
32
- "Content-Type": "application/json"
33
- },
34
- body: {
35
- "model": Sublayer.configuration.ai_model,
36
- "messages": [
37
- { "role": "user", "content": "#{system_prompt}\n#{prompt}}" }
38
- ]
39
- }.to_json
40
- )
41
-
42
- text_containing_xml = response.dig("choices", 0, "message", "content")
43
- tool_output = Nokogiri::HTML.parse(text_containing_xml.match(/\<#{output_adapter.name}\>(.*?)\<\/#{output_adapter.name}\>/m)[1]).text
44
- raise "The response was not formatted correctly: #{response.body}" unless tool_output
45
-
46
- return tool_output
47
- end
48
- end
49
- end
50
- end