Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add list of strings output adapter and update providers to handle the new attribute #47

Merged
merged 3 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions lib/sublayer/components/output_adapters/list_of_strings.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module Sublayer
module Components
module OutputAdapters
class ListOfStrings
attr_reader :name, :description

def initialize(options)
@name = options[:name]
@description = options[:description]
end

def properties
[
OpenStruct.new(
name: @name,
type: 'array',
description: @description,
required: true,
items: {
type: 'string'
}
)
]
end
end
end
end
end
81 changes: 39 additions & 42 deletions lib/sublayer/providers/gemini.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,34 @@ module Sublayer
module Providers
class Gemini
def self.call(prompt:, output_adapter:)
system_prompt = <<-PROMPT
You have access to a set of tools to answer the prompt.

You may call tools like this:
<tool_calls>
<tool_call>
<tool_name>$TOOL_NAME</tool_name>
<parameters>
<$PARAMETER_NAME>$VALUE</$PARAMETER_NAME>
...
</parameters>
</tool_call>
</tool_calls>

Here are the tools available:
<tools>
<tool_description>
<tool_name>#{output_adapter.name}</tool_name>
<tool_description>#{output_adapter.description}</tool_description>
<parameters>
#{format_properties(output_adapter)}
</parameters>
</tool_description>
</tools>

Respond only with valid xml.
The entire response should be wrapped in a <response> tag.
Your response should call a tool inside a <tool_calls> tag.
PROMPT

response = HTTParty.post(
"https://generativelanguage.googleapis.com/v1beta/models/#{Sublayer.configuration.ai_model}:generateContent?key=#{ENV['GEMINI_API_KEY']}",
body: {
contents: { role: "user", parts: { text: "#{system_prompt}\n#{prompt}" } }
contents: {
role: "user",
parts: {
text: "#{prompt}"
},
},
tools: {
functionDeclarations: [
{
name: output_adapter.name,
description: output_adapter.description,
parameters: {
type: "OBJECT",
properties: format_properties(output_adapter),
required: output_adapter.properties.select(&:required).map(&:name)
}
}
]
},
tool_config: {
function_calling_config: {
mode: "ANY",
allowed_function_names: [output_adapter.name]
}
}
}.to_json,
headers: {
"Content-Type" => "application/json"
Expand All @@ -47,21 +41,24 @@ def self.call(prompt:, output_adapter:)

raise "Error generating with Gemini, error: #{response.body}" unless response.success?

text_containing_xml = response.dig('candidates', 0, 'content', 'parts', 0, 'text')
tool_output = Nokogiri::HTML.parse(text_containing_xml.match(/\<#{output_adapter.name}\>(.*?)\<\/#{output_adapter.name}\>/m)[1]).text

raise "Gemini did not format response, error: #{response.body}" unless tool_output
return tool_output
argument = response.dig("candidates", 0, "content", "parts", 0, "functionCall", "args", output_adapter.name)
end

private
def self.format_properties(output_adapter)
output_adapter.properties.each_with_object("") do |property, xml|
xml << "<name>#{property.name}</name>"
xml << "<type>#{property.type}</type>"
xml << "<description>#{property.description}</description>"
xml << "<required>#{property.required}</required>"
xml << "<enum>#{property.enum}</enum>" if property.enum
output_adapter.properties.each_with_object({}) do |property, hash|
hash[property.name] = {
type: property.type.upcase,
description: property.description
}

if property.enum
hash[property.name][:enum] = property.enum
end

if property.items
hash[property.name][:items] = property.items
end
end
end
end
Expand Down
4 changes: 4 additions & 0 deletions lib/sublayer/providers/open_ai.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ def self.format_properties(output_adapter)
if property.enum
hash[property.name][:enum] = property.enum
end

if property.items
hash[property.name][:items] = property.items
end
end
end
end
Expand Down
37 changes: 37 additions & 0 deletions spec/components/output_adapters/list_of_strings_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
require "spec_helper"

RSpec.describe Sublayer::Components::OutputAdapters::ListOfStrings do
describe "#initialize" do
it "sets the name and description" do
adapter = Sublayer::Components::OutputAdapters::ListOfStrings.new(name: "test_list", description: "A test list of strings")

expect(adapter.name).to eq("test_list")
expect(adapter.description).to eq("A test list of strings")
end
end

describe "#properties" do
it "returns an array with one item" do
adapter = Sublayer::Components::OutputAdapters::ListOfStrings.new(name: "test_list", description: "A test list of strings")

expect(adapter.properties).to be_an(Array)
expect(adapter.properties.size).to eq(1)
end

it "returns an OpenStruct object" do
adapter = Sublayer::Components::OutputAdapters::ListOfStrings.new(name: "test_list", description: "A test list of strings")

expect(adapter.properties.first).to be_an(OpenStruct)
end

it "has the correct attributes" do
adapter = Sublayer::Components::OutputAdapters::ListOfStrings.new(name: "test_list", description: "A test list of strings")

expect(adapter.properties.first.name).to eq("test_list")
expect(adapter.properties.first.type).to eq("array")
expect(adapter.properties.first.description).to eq("A test list of strings")
expect(adapter.properties.first.required).to eq(true)
expect(adapter.properties.first.items).to eq( {type: "string"} )
end
end
end
52 changes: 52 additions & 0 deletions spec/generators/blog_post_keyword_suggestions_generator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
require "spec_helper"

require "generators/examples/blog_post_keyword_suggestions_generator"

RSpec.describe BlogPostKeywordSuggestionGenerator do
let(:topic) { "Artificial Intelligence in Healthcare" }
let(:num_keywords) { 5 }

subject { described_class.new(topic: topic, num_keywords: num_keywords) }

context "claude" do
before do
Sublayer.configuration.ai_provider = Sublayer::Providers::Claude
Sublayer.configuration.ai_model = "claude-3-5-sonnet-20240620"
end

it "generates keyword suggestions for a blog post" do
VCR.use_cassette("claude/generators/blog_post_keyword_suggestions_generator/ai_in_healthcare") do
keywords = subject.generate
expect(keywords).to be_an_instance_of(Array)
end
end
end

context "openai" do
before do
Sublayer.configuration.ai_provider = Sublayer::Providers::OpenAI
Sublayer.configuration.ai_model = "gpt-4o"
end

it "generates keyword suggestions for a blog post" do
VCR.use_cassette("openai/generators/blog_post_keyword_suggestions_generator/ai_in_healthcare") do
keywords = subject.generate
expect(keywords).to be_an_instance_of(Array)
end
end
end

context "gemini" do
before do
Sublayer.configuration.ai_provider = Sublayer::Providers::Gemini
Sublayer.configuration.ai_model = "gemini-pro"
end

it "generates keyword suggestions for a blog post" do
VCR.use_cassette("gemini/generators/blog_post_keyword_suggestions_generator/ai_in_healthcare") do
keywords = subject.generate
expect(keywords).to be_an_instance_of(Array)
end
end
end
end
2 changes: 1 addition & 1 deletion spec/generators/code_from_description_generator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def generate(description:, technologies: ["ruby"])

end

context "Gemini" do
xcontext "Gemini" do
before do
Sublayer.configuration.ai_provider = Sublayer::Providers::Gemini
Sublayer.configuration.ai_model = "gemini-pro"
Expand Down
4 changes: 2 additions & 2 deletions spec/generators/description_from_code_generator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def generate(code)
context "Gemini" do
before do
Sublayer.configuration.ai_provider = Sublayer::Providers::Gemini
Sublayer.configuration.ai_model = "gemini-pro"
Sublayer.configuration.ai_model = "gemini-1.5-pro-latest"
end

it "generates description from hello world code" do
Expand All @@ -99,7 +99,7 @@ def generate(code)

description = generate(code)
expect(description.strip).to eq <<~DESCRIPTION.strip
This code is a simple command-line script that greets a person by name. It takes an optional argument, `--who`, to specify the name of the person to greet, and defaults to \"world\" if no name is provided. The script then prints a greeting to the specified person.
This Ruby code is a simple command-line program that greets a person by name. \\n\\nHere is a breakdown of the code:\\n\\n1. **Requires the `optparse` library:** This line includes the `optparse` library, which is used for parsing command-line options.\\n2. **Initializes an options hash:** `options = {}` creates an empty hash called `options` to store command-line arguments.\\n3. **Defines command-line options:**\\n - `OptionParser.new do |opts| ... end.parse!` creates a new OptionParser object and defines the command-line options.\\n - `opts.banner = \"Usage: hello.rb [options]\"` sets the banner message displayed at the top of the help text.\\n - `opts.on(\"-w\", \"--who PERSON\", \"Name of the person to greet\") do |person| ... end` defines an option `-w` or `--who` that takes a `PERSON` argument. The value of the argument is stored in the `options[:who]` hash.\\n4. **Gets the name to greet:**\\n - `who = options[:who] || \"world\"` retrieves the value of the `:who` option from the `options` hash. If the option is not provided, it defaults to \"world\".\\n5. **Prints the greeting:**\\n - `puts \"Hello, \#{who}!\"` prints the greeting message with the name of the person being greeted.
DESCRIPTION
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class BlogPostKeywordSuggestionGenerator < Sublayer::Generators::Base
llm_output_adapter type: :list_of_strings,
name: "suggestions",
description: "List of keyword suggestions"

def initialize(topic:, num_keywords: 5)
@topic = topic
end

def generate
super
end

def prompt
<<-PROMPT
You are an SEO expect tasked with suggesting keywords for a blog post.

The blog post topic is: #{@topic}

Please suggest relevant #{@num_keywords} keywords or key phrases for this post's topic.
Each keyword or phrase should be concise and directly related to the topic.

Provide your suggestions as a list of strings.
PROMPT
end
end
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class DescriptionFromCodeGenerator < Sublayer::Generators::Base
llm_output_adapter type: :single_string,
name: "code_description",
description: "A description of what the code in the file does"
description: "A description of what the code does, its purpose,functionality, and any noteworthy details"

def initialize(code:)
@code = code
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading