From 02e30ec8583a6b84f9a7babaad0763f226b49f8c Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Mon, 22 Jul 2024 17:57:11 -0400 Subject: [PATCH 1/2] Add list of strings output adapter along with tests --- .../output_adapters/list_of_strings.rb | 28 +++++ lib/sublayer/providers/gemini.rb | 81 ++++++------ lib/sublayer/providers/open_ai.rb | 4 + .../output_adapters/list_of_strings_spec.rb | 37 ++++++ ...post_keyword_suggestions_generator_spec.rb | 52 ++++++++ .../description_from_code_generator_spec.rb | 4 +- ...blog_post_keyword_suggestions_generator.rb | 26 ++++ .../description_from_code_generator.rb | 2 +- .../ai_in_healthcare.yml | 81 ++++++++++++ .../ai_in_healthcare.yml | 106 ++++++++++++++++ .../hello_world.yml | 36 +++--- .../hello_world.yml | 55 +++++---- .../route.yml | 43 ++++--- .../positive.yml | 45 ++++--- .../ai_in_healthcare.yml | 115 ++++++++++++++++++ 15 files changed, 583 insertions(+), 132 deletions(-) create mode 100644 lib/sublayer/components/output_adapters/list_of_strings.rb create mode 100644 spec/components/output_adapters/list_of_strings_spec.rb create mode 100644 spec/generators/blog_post_keyword_suggestions_generator_spec.rb create mode 100644 spec/generators/examples/blog_post_keyword_suggestions_generator.rb create mode 100644 spec/vcr_cassettes/claude/generators/blog_post_keyword_suggestions_generator/ai_in_healthcare.yml create mode 100644 spec/vcr_cassettes/gemini/generators/blog_post_keyword_suggestions_generator/ai_in_healthcare.yml create mode 100644 spec/vcr_cassettes/openai/generators/blog_post_keyword_suggestions_generator/ai_in_healthcare.yml diff --git a/lib/sublayer/components/output_adapters/list_of_strings.rb b/lib/sublayer/components/output_adapters/list_of_strings.rb new file mode 100644 index 0000000..42bd665 --- /dev/null +++ b/lib/sublayer/components/output_adapters/list_of_strings.rb @@ -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 diff --git a/lib/sublayer/providers/gemini.rb b/lib/sublayer/providers/gemini.rb index e5f5da1..e580448 100644 --- a/lib/sublayer/providers/gemini.rb +++ b/lib/sublayer/providers/gemini.rb @@ -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_NAME - - <$PARAMETER_NAME>$VALUE - ... - - - - - Here are the tools available: - - - #{output_adapter.name} - #{output_adapter.description} - - #{format_properties(output_adapter)} - - - - - Respond only with valid xml. - The entire response should be wrapped in a tag. - Your response should call a tool inside a 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" @@ -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 << "#{property.name}" - xml << "#{property.type}" - xml << "#{property.description}" - xml << "#{property.required}" - xml << "#{property.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 diff --git a/lib/sublayer/providers/open_ai.rb b/lib/sublayer/providers/open_ai.rb index d1527f9..c492f37 100644 --- a/lib/sublayer/providers/open_ai.rb +++ b/lib/sublayer/providers/open_ai.rb @@ -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 diff --git a/spec/components/output_adapters/list_of_strings_spec.rb b/spec/components/output_adapters/list_of_strings_spec.rb new file mode 100644 index 0000000..dca90f7 --- /dev/null +++ b/spec/components/output_adapters/list_of_strings_spec.rb @@ -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 diff --git a/spec/generators/blog_post_keyword_suggestions_generator_spec.rb b/spec/generators/blog_post_keyword_suggestions_generator_spec.rb new file mode 100644 index 0000000..9b8be96 --- /dev/null +++ b/spec/generators/blog_post_keyword_suggestions_generator_spec.rb @@ -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 diff --git a/spec/generators/description_from_code_generator_spec.rb b/spec/generators/description_from_code_generator_spec.rb index b5b284f..66143af 100644 --- a/spec/generators/description_from_code_generator_spec.rb +++ b/spec/generators/description_from_code_generator_spec.rb @@ -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 @@ -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 diff --git a/spec/generators/examples/blog_post_keyword_suggestions_generator.rb b/spec/generators/examples/blog_post_keyword_suggestions_generator.rb new file mode 100644 index 0000000..3210d02 --- /dev/null +++ b/spec/generators/examples/blog_post_keyword_suggestions_generator.rb @@ -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 diff --git a/spec/generators/examples/description_from_code_generator.rb b/spec/generators/examples/description_from_code_generator.rb index d18d3c6..fa07780 100644 --- a/spec/generators/examples/description_from_code_generator.rb +++ b/spec/generators/examples/description_from_code_generator.rb @@ -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 diff --git a/spec/vcr_cassettes/claude/generators/blog_post_keyword_suggestions_generator/ai_in_healthcare.yml b/spec/vcr_cassettes/claude/generators/blog_post_keyword_suggestions_generator/ai_in_healthcare.yml new file mode 100644 index 0000000..be2a46c --- /dev/null +++ b/spec/vcr_cassettes/claude/generators/blog_post_keyword_suggestions_generator/ai_in_healthcare.yml @@ -0,0 +1,81 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-3-5-sonnet-20240620","max_tokens":4096,"tools":[{"name":"suggestions","description":"List + of keyword suggestions","input_schema":{"type":"object","properties":{"suggestions":{"type":"array","description":"List + of keyword suggestions"}},"required":["suggestions"]}}],"messages":[{"role":"user","content":" You + are an SEO expect tasked with suggesting keywords for a blog post.\n\n The + blog post topic is: Artificial Intelligence in Healthcare\n\n Please suggest + relevant keywords or key phrases for this post''s topic.\n Each keyword + or phrase should be concise and directly related to the topic.\n\n Provide + your suggestions as a list of strings.\n"}]}' + headers: + X-Api-Key: + - "" + Anthropic-Version: + - '2023-06-01' + Content-Type: + - application/json + Anthropic-Beta: + - tools-2024-04-04 + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Date: + - Mon, 22 Jul 2024 20:48:35 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Requests-Limit: + - '1000' + Anthropic-Ratelimit-Requests-Remaining: + - '1000' + Anthropic-Ratelimit-Requests-Reset: + - '2024-07-22T20:49:34Z' + Anthropic-Ratelimit-Tokens-Limit: + - '80000' + Anthropic-Ratelimit-Tokens-Remaining: + - '80000' + Anthropic-Ratelimit-Tokens-Reset: + - '2024-07-22T20:48:35Z' + Request-Id: + - req_01Fef9iyNDDjYMbrZpXDiHYa + X-Cloud-Trace-Context: + - 667a997add66741ea898f264c8bd534f + Via: + - 1.1 google + Cf-Cache-Status: + - DYNAMIC + Server: + - cloudflare + Cf-Ray: + - 8a764ac5e95cc324-EWR + body: + encoding: ASCII-8BIT + string: '{"id":"msg_01LuPH8dGCEdLkZHaK7rDLkz","type":"message","role":"assistant","model":"claude-3-5-sonnet-20240620","content":[{"type":"text","text":"Certainly! + As an SEO expert, I''d be happy to suggest relevant keywords and key phrases + for a blog post about Artificial Intelligence in Healthcare. To provide you + with the most accurate and useful suggestions, I''ll use the \"suggestions\" + function to generate a list of keywords. Let me do that for you now."},{"type":"tool_use","id":"toolu_01A7Vm7bVWpQsTPSwpEasnJE","name":"suggestions","input":{"suggestions":["AI + in healthcare","medical AI applications","machine learning in medicine","healthcare + automation","AI diagnosis","predictive healthcare","AI-powered medical imaging","robotic + surgery","AI drug discovery","personalized medicine AI","healthcare data analytics","AI + patient monitoring","electronic health records AI","AI in telemedicine","medical + chatbots","AI-assisted clinical decision making"]}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":435,"output_tokens":204}}' + recorded_at: Mon, 22 Jul 2024 20:48:35 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/vcr_cassettes/gemini/generators/blog_post_keyword_suggestions_generator/ai_in_healthcare.yml b/spec/vcr_cassettes/gemini/generators/blog_post_keyword_suggestions_generator/ai_in_healthcare.yml new file mode 100644 index 0000000..a93b6ca --- /dev/null +++ b/spec/vcr_cassettes/gemini/generators/blog_post_keyword_suggestions_generator/ai_in_healthcare.yml @@ -0,0 +1,106 @@ +--- +http_interactions: +- request: + method: post + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key= + body: + encoding: UTF-8 + string: '{"contents":{"role":"user","parts":{"text":" You are an SEO expect + tasked with suggesting keywords for a blog post.\n\n The blog post topic + is: Artificial Intelligence in Healthcare\n\n Please suggest relevant keywords + or key phrases for this post''s topic.\n Each keyword or phrase should + be concise and directly related to the topic.\n\n Provide your suggestions + as a list of strings.\n"}},"tools":{"functionDeclarations":[{"name":"suggestions","description":"List + of keyword suggestions","parameters":{"type":"OBJECT","properties":{"suggestions":{"type":"ARRAY","description":"List + of keyword suggestions","items":{"type":"string"}}},"required":["suggestions"]}}]}}' + headers: + Content-Type: + - application/json + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json; charset=UTF-8 + Vary: + - Origin + - Referer + - X-Origin + Date: + - Mon, 22 Jul 2024 21:29:06 GMT + Server: + - scaffolding on HTTPServer2 + Cache-Control: + - private + X-Xss-Protection: + - '0' + X-Frame-Options: + - SAMEORIGIN + X-Content-Type-Options: + - nosniff + Server-Timing: + - gfet4t7; dur=1195 + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Transfer-Encoding: + - chunked + body: + encoding: ASCII-8BIT + string: | + { + "candidates": [ + { + "content": { + "parts": [ + { + "functionCall": { + "name": "suggestions", + "args": { + "suggestions": [ + "Artificial Intelligence in Healthcare", + "cancer", + "disease" + ] + } + } + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "usageMetadata": { + "promptTokenCount": 112, + "candidatesTokenCount": 22, + "totalTokenCount": 134 + } + } + recorded_at: Mon, 22 Jul 2024 21:29:06 GMT +recorded_with: VCR 6.2.0 diff --git a/spec/vcr_cassettes/gemini/generators/code_from_description_generator/hello_world.yml b/spec/vcr_cassettes/gemini/generators/code_from_description_generator/hello_world.yml index 45ee8ce..c657345 100644 --- a/spec/vcr_cassettes/gemini/generators/code_from_description_generator/hello_world.yml +++ b/spec/vcr_cassettes/gemini/generators/code_from_description_generator/hello_world.yml @@ -5,19 +5,14 @@ http_interactions: uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key= body: encoding: UTF-8 - string: '{"contents":{"role":"user","parts":{"text":" You have access - to a set of tools to answer the prompt.\n\n You may call tools like - this:\n \n \n $TOOL_NAME\n \n <$PARAMETER_NAME>$VALUE\n ...\n \n \n \n\n Here - are the tools available:\n \n \n generated_code\n The - generated code in the requested language\n \n generated_codestringThe - generated code in the requested language\n \n \n \n\n Respond - only with valid xml.\n The entire response should be wrapped in a - tag.\n Your response should call a tool inside a tag.\n\n You - are an expert programmer in ruby.\n\n You are tasked with writing code - using the following technologies: ruby.\n\n The description of the - task is a hello world app where I pass --who argument to set the ''world'' - value using optparser\n\n Take a deep breath and think step by step - before you start coding.\n"}}}' + string: '{"contents":{"role":"user","parts":{"text":" You are an expert + programmer in ruby.\n\n You are tasked with writing code using the + following technologies: ruby.\n\n The description of the task is a + hello world app where I pass --who argument to set the ''world'' value using + optparser\n\n Take a deep breath and think step by step before you + start coding.\n"}},"tools":{"functionDeclarations":[{"name":"generated_code","description":"The + generated code in the requested language","parameters":{"type":"OBJECT","properties":{"generated_code":{"type":"STRING","description":"The + generated code in the requested language"}},"required":["generated_code"]}}]},"tool_config":{"function_calling_config":{"mode":"ANY","allowed_function_names":["generated_code"]}}}' headers: Content-Type: - application/json @@ -39,7 +34,7 @@ http_interactions: - Referer - X-Origin Date: - - Tue, 30 Apr 2024 17:33:56 GMT + - Mon, 22 Jul 2024 21:55:02 GMT Server: - scaffolding on HTTPServer2 Cache-Control: @@ -51,7 +46,7 @@ http_interactions: X-Content-Type-Options: - nosniff Server-Timing: - - gfet4t7; dur=3071 + - gfet4t7; dur=2536 Alt-Svc: - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Transfer-Encoding: @@ -65,7 +60,7 @@ http_interactions: "content": { "parts": [ { - "text": "```xml\n\u003cresponse\u003e\n \u003ctool_calls\u003e\n \u003ctool_call\u003e\n \u003ctool_name\u003egenerated_code\u003c/tool_name\u003e\n \u003cparameters\u003e\n \u003cgenerated_code\u003e#!/usr/bin/env ruby\n\nrequire 'optparse'\n\noptions = {}\nOptionParser.new do |opts|\n opts.on('--who WHO', 'Who to greet (default: World)') do |who|\n options[:who] = who\n end\nend.parse!\n\nputs \"Hello, #{options[:who]}!\"\u003c/generated_code\u003e\n \u003c/parameters\u003e\n \u003c/tool_call\u003e\n \u003c/tool_calls\u003e\n\u003c/response\u003e\n```" + "text": "```ruby \n require \"optparse\"\n\n # Define the options for the script\n options = {}\n OptionParser.new do |opts|\n opts.on(\"-w\", \"--who NAME\", \"Who to say hello to (default: World)\") do |who|\n options[:who] = who\n end\n end.parse!\n\n # Set the default value for the `who` option\n options[:who] ||= \"World\"\n\n # Print the greeting\n puts \"Hello, #{options[:who]}!\" \n```" } ], "role": "model" @@ -91,7 +86,12 @@ http_interactions: } ] } - ] + ], + "usageMetadata": { + "promptTokenCount": 119, + "candidatesTokenCount": 122, + "totalTokenCount": 241 + } } - recorded_at: Tue, 30 Apr 2024 17:33:56 GMT + recorded_at: Mon, 22 Jul 2024 21:55:02 GMT recorded_with: VCR 6.2.0 diff --git a/spec/vcr_cassettes/gemini/generators/description_from_code_generator/hello_world.yml b/spec/vcr_cassettes/gemini/generators/description_from_code_generator/hello_world.yml index b021edb..aa8cc73 100644 --- a/spec/vcr_cassettes/gemini/generators/description_from_code_generator/hello_world.yml +++ b/spec/vcr_cassettes/gemini/generators/description_from_code_generator/hello_world.yml @@ -2,25 +2,22 @@ http_interactions: - request: method: post - uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key= + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro-latest:generateContent?key= body: encoding: UTF-8 - string: '{"contents":{"role":"user","parts":{"text":" You have access - to a set of tools to answer the prompt.\n\n You may call tools like - this:\n \n \n $TOOL_NAME\n \n <$PARAMETER_NAME>$VALUE\n ...\n \n \n \n\n Here - are the tools available:\n \n \n code_description\n A - description of what the code in the file does\n \n code_descriptionstringA - description of what the code in the file does\n \n \n \n\n Respond - only with valid xml.\n The entire response should be wrapped in a - tag.\n Your response should call a tool inside a tag.\n\n You - are an experienced software engineer. Below is a chunk of code:\n\n #!/usr/bin/env - ruby\n\n require ''optparse''\n\n options = {}\n OptionParser.new - do |opts|\n opts.banner = \"Usage: hello.rb [options]\"\n\n opts.on(\"-w\", - \"--who PERSON\", \"Name of the person to greet\") do |person|\n options[:who] - = person\n end\n end.parse!\n\n who = options[:who] - || \"world\"\n puts \"Hello, #{who}!\"\n\n Please read the code + string: '{"contents":{"role":"user","parts":{"text":" You are an experienced + software engineer. Below is a chunk of code:\n\n #!/usr/bin/env ruby\n\n require + ''optparse''\n\n options = {}\n OptionParser.new do |opts|\n opts.banner + = \"Usage: hello.rb [options]\"\n\n opts.on(\"-w\", \"--who PERSON\", + \"Name of the person to greet\") do |person|\n options[:who] = + person\n end\n end.parse!\n\n who = options[:who] || + \"world\"\n puts \"Hello, #{who}!\"\n\n Please read the code carefully and provide a high-level description of what this code does, including - its purpose, functionalities, and any noteworthy details.\n"}}}' + its purpose, functionalities, and any noteworthy details.\n"}},"tools":{"functionDeclarations":[{"name":"code_description","description":"A + description of what the code does, its purpose,functionality, and any noteworthy + details","parameters":{"type":"OBJECT","properties":{"code_description":{"type":"STRING","description":"A + description of what the code does, its purpose,functionality, and any noteworthy + details"}},"required":["code_description"]}}]},"tool_config":{"function_calling_config":{"mode":"ANY","allowed_function_names":["code_description"]}}}' headers: Content-Type: - application/json @@ -42,7 +39,7 @@ http_interactions: - Referer - X-Origin Date: - - Tue, 30 Apr 2024 17:41:29 GMT + - Mon, 22 Jul 2024 21:43:47 GMT Server: - scaffolding on HTTPServer2 Cache-Control: @@ -54,7 +51,7 @@ http_interactions: X-Content-Type-Options: - nosniff Server-Timing: - - gfet4t7; dur=2840 + - gfet4t7; dur=7267 Alt-Svc: - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Transfer-Encoding: @@ -68,7 +65,12 @@ http_interactions: "content": { "parts": [ { - "text": "\u003cresponse\u003e\n \u003ctool_calls\u003e\n \u003ctool_call\u003e\n \u003ctool_name\u003ecode_description\u003c/tool_name\u003e\n \u003cparameters\u003e\n \u003ccode_description\u003eThis 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.\u003c/code_description\u003e\n \u003c/parameters\u003e\n \u003c/tool_call\u003e\n \u003c/tool_calls\u003e\n\u003c/response\u003e" + "functionCall": { + "name": "code_description", + "args": { + "code_description": "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." + } + } } ], "role": "model" @@ -77,11 +79,11 @@ http_interactions: "index": 0, "safetyRatings": [ { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "probability": "NEGLIGIBLE" }, { - "category": "HARM_CATEGORY_HATE_SPEECH", + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability": "NEGLIGIBLE" }, { @@ -89,12 +91,17 @@ http_interactions: "probability": "NEGLIGIBLE" }, { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "category": "HARM_CATEGORY_HATE_SPEECH", "probability": "NEGLIGIBLE" } ] } - ] + ], + "usageMetadata": { + "promptTokenCount": 228, + "candidatesTokenCount": 346, + "totalTokenCount": 574 + } } - recorded_at: Tue, 30 Apr 2024 17:41:29 GMT + recorded_at: Mon, 22 Jul 2024 21:43:47 GMT recorded_with: VCR 6.2.0 diff --git a/spec/vcr_cassettes/gemini/generators/route_selection_from_user_intent_generator/route.yml b/spec/vcr_cassettes/gemini/generators/route_selection_from_user_intent_generator/route.yml index 3a38b9c..9b435b3 100644 --- a/spec/vcr_cassettes/gemini/generators/route_selection_from_user_intent_generator/route.yml +++ b/spec/vcr_cassettes/gemini/generators/route_selection_from_user_intent_generator/route.yml @@ -5,19 +5,13 @@ http_interactions: uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key= body: encoding: UTF-8 - string: '{"contents":{"role":"user","parts":{"text":" You have access - to a set of tools to answer the prompt.\n\n You may call tools like - this:\n \n \n $TOOL_NAME\n \n <$PARAMETER_NAME>$VALUE\n ...\n \n \n \n\n Here - are the tools available:\n \n \n route\n A - route selected from the list\n \n routestringA - route selected from the listtrue[\"GET - /\", \"GET /users\", \"GET /users/:id\", \"POST /users\", \"PUT /users/:id\", - \"DELETE /users/:id\"]\n \n \n \n\n Respond - only with valid xml.\n The entire response should be wrapped in a - tag.\n Your response should call a tool inside a tag.\n\n You - are skilled at selecting routes based on user intent.\n\n Your task - is to choose a route based on the following intent:\n\n The user''s - intent is:\n I want to get all the users\n"}}}' + string: '{"contents":{"role":"user","parts":{"text":" You are skilled + at selecting routes based on user intent.\n\n Your task is to choose + a route based on the following intent:\n\n The user''s intent is:\n I + want to get all the users\n"}},"tools":{"functionDeclarations":[{"name":"route","description":"A + route selected from the list","parameters":{"type":"OBJECT","properties":{"route":{"type":"STRING","description":"A + route selected from the list","enum":["GET /","GET /users","GET /users/:id","POST + /users","PUT /users/:id","DELETE /users/:id"]}},"required":["route"]}}]}}' headers: Content-Type: - application/json @@ -39,7 +33,7 @@ http_interactions: - Referer - X-Origin Date: - - Thu, 16 May 2024 17:50:12 GMT + - Mon, 22 Jul 2024 21:35:07 GMT Server: - scaffolding on HTTPServer2 Cache-Control: @@ -51,7 +45,7 @@ http_interactions: X-Content-Type-Options: - nosniff Server-Timing: - - gfet4t7; dur=1748 + - gfet4t7; dur=857 Alt-Svc: - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Transfer-Encoding: @@ -65,7 +59,12 @@ http_interactions: "content": { "parts": [ { - "text": "\u003cresponse\u003e\n \u003ctool_calls\u003e\n \u003ctool_call\u003e\n \u003ctool_name\u003eroute\u003c/tool_name\u003e\n \u003cparameters\u003e\n \u003croute\u003eGET /users\u003c/route\u003e\n \u003c/parameters\u003e\n \u003c/tool_call\u003e\n \u003c/tool_calls\u003e\n\u003c/response\u003e" + "functionCall": { + "name": "route", + "args": { + "route": "GET /users" + } + } } ], "role": "model" @@ -82,21 +81,21 @@ http_interactions: "probability": "NEGLIGIBLE" }, { - "category": "HARM_CATEGORY_HARASSMENT", + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "probability": "NEGLIGIBLE" }, { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "category": "HARM_CATEGORY_HARASSMENT", "probability": "NEGLIGIBLE" } ] } ], "usageMetadata": { - "promptTokenCount": 326, - "candidatesTokenCount": 69, - "totalTokenCount": 395 + "promptTokenCount": 122, + "candidatesTokenCount": 15, + "totalTokenCount": 137 } } - recorded_at: Thu, 16 May 2024 17:50:12 GMT + recorded_at: Mon, 22 Jul 2024 21:35:07 GMT recorded_with: VCR 6.2.0 diff --git a/spec/vcr_cassettes/gemini/generators/sentiment_from_text_generator/positive.yml b/spec/vcr_cassettes/gemini/generators/sentiment_from_text_generator/positive.yml index fb09d66..db3766a 100644 --- a/spec/vcr_cassettes/gemini/generators/sentiment_from_text_generator/positive.yml +++ b/spec/vcr_cassettes/gemini/generators/sentiment_from_text_generator/positive.yml @@ -5,18 +5,12 @@ http_interactions: uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key= body: encoding: UTF-8 - string: '{"contents":{"role":"user","parts":{"text":" You have access - to a set of tools to answer the prompt.\n\n You may call tools like - this:\n \n \n $TOOL_NAME\n \n <$PARAMETER_NAME>$VALUE\n ...\n \n \n \n\n Here - are the tools available:\n \n \n sentiment_value\n A - sentiment value from the list\n \n sentiment_valuestringA - sentiment value from the listtrue[\"positive\", - \"negative\", \"neutral\"]\n \n \n \n\n Respond - only with valid xml.\n The entire response should be wrapped in a - tag.\n Your response should call a tool inside a tag.\n\n You - are an expert at determining sentiment from text.\n\n You are tasked - with analyzing the following text and determining its sentiment value.\n\n The - text is:\n Matz is nice so we are nice\n"}}}' + string: '{"contents":{"role":"user","parts":{"text":" You are an expert + at determining sentiment from text.\n\n You are tasked with analyzing + the following text and determining its sentiment value.\n\n The text + is:\n Matz is nice so we are nice\n"}},"tools":{"functionDeclarations":[{"name":"sentiment_value","description":"A + sentiment value from the list","parameters":{"type":"OBJECT","properties":{"sentiment_value":{"type":"STRING","description":"A + sentiment value from the list","enum":["positive","negative","neutral"]}},"required":["sentiment_value"]}}]}}' headers: Content-Type: - application/json @@ -38,7 +32,7 @@ http_interactions: - Referer - X-Origin Date: - - Wed, 15 May 2024 21:27:46 GMT + - Mon, 22 Jul 2024 21:35:09 GMT Server: - scaffolding on HTTPServer2 Cache-Control: @@ -50,7 +44,7 @@ http_interactions: X-Content-Type-Options: - nosniff Server-Timing: - - gfet4t7; dur=1777 + - gfet4t7; dur=958 Alt-Svc: - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 Transfer-Encoding: @@ -64,7 +58,12 @@ http_interactions: "content": { "parts": [ { - "text": "\u003cresponse\u003e\n \u003ctool_calls\u003e\n \u003ctool_call\u003e\n \u003ctool_name\u003esentiment_value\u003c/tool_name\u003e\n \u003cparameters\u003e\n \u003csentiment_value\u003epositive\u003c/sentiment_value\u003e\n \u003c/parameters\u003e\n \u003c/tool_call\u003e\n \u003c/tool_calls\u003e\n\u003c/response\u003e" + "functionCall": { + "name": "sentiment_value", + "args": { + "sentiment_value": "positive" + } + } } ], "role": "model" @@ -73,29 +72,29 @@ http_interactions: "index": 0, "safetyRatings": [ { - "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", "probability": "NEGLIGIBLE" }, { - "category": "HARM_CATEGORY_HATE_SPEECH", + "category": "HARM_CATEGORY_HARASSMENT", "probability": "NEGLIGIBLE" }, { - "category": "HARM_CATEGORY_HARASSMENT", + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "probability": "NEGLIGIBLE" }, { - "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "category": "HARM_CATEGORY_HATE_SPEECH", "probability": "NEGLIGIBLE" } ] } ], "usageMetadata": { - "promptTokenCount": 302, - "candidatesTokenCount": 73, - "totalTokenCount": 375 + "promptTokenCount": 100, + "candidatesTokenCount": 17, + "totalTokenCount": 117 } } - recorded_at: Wed, 15 May 2024 21:27:46 GMT + recorded_at: Mon, 22 Jul 2024 21:35:09 GMT recorded_with: VCR 6.2.0 diff --git a/spec/vcr_cassettes/openai/generators/blog_post_keyword_suggestions_generator/ai_in_healthcare.yml b/spec/vcr_cassettes/openai/generators/blog_post_keyword_suggestions_generator/ai_in_healthcare.yml new file mode 100644 index 0000000..732c762 --- /dev/null +++ b/spec/vcr_cassettes/openai/generators/blog_post_keyword_suggestions_generator/ai_in_healthcare.yml @@ -0,0 +1,115 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"gpt-4o","messages":[{"role":"user","content":" You are + an SEO expect tasked with suggesting keywords for a blog post.\n\n The + blog post topic is: Artificial Intelligence in Healthcare\n\n Please suggest + relevant keywords or key phrases for this post''s topic.\n Each keyword + or phrase should be concise and directly related to the topic.\n\n Provide + your suggestions as a list of strings.\n"}],"tool_choice":{"type":"function","function":{"name":"suggestions"}},"tools":[{"type":"function","function":{"name":"suggestions","description":"List + of keyword suggestions","parameters":{"type":"object","properties":{"suggestions":{"type":"array","description":"List + of keyword suggestions","items":{"type":"string"}}}},"required":[["suggestions"]]}}]}' + headers: + Content-Type: + - application/json + Authorization: + - Bearer + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - "*/*" + User-Agent: + - Ruby + response: + status: + code: 200 + message: OK + headers: + Date: + - Mon, 22 Jul 2024 20:48:27 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Openai-Organization: + - sublayer + Openai-Processing-Ms: + - '824' + Openai-Version: + - '2020-10-01' + Strict-Transport-Security: + - max-age=15552000; includeSubDomains; preload + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '2000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '1999895' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 3ms + X-Request-Id: + - req_67904025de6274fd32ed84708988733d + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=JWzDWTjx34t5BwJdodcGalH42MiKFZwx4RxFE52cy2Y-1721681307-1.0.1.1-5f60RrnoVzdQuQP2.tv6s7FVWqMx7TCRbES2eAglaZB7Yk6boMGWsXIwxcehbwGgLY1jaFtrD0GWv4q.g.RLjQ; + path=/; expires=Mon, 22-Jul-24 21:18:27 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=XryDES2JpLz5wmau2kZzbeIUfNAJPP3FQdnjlQfqKP4-1721681307408-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 8a764aa428d57d18-EWR + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-9nuFGyQnPvfvtAEz3lcYgtFhirdsm", + "object": "chat.completion", + "created": 1721681306, + "model": "gpt-4o-2024-05-13", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_ZsvytffImSm5Dlxy16BT8XGj", + "type": "function", + "function": { + "name": "suggestions", + "arguments": "{\"suggestions\":[\"Artificial Intelligence in Healthcare\",\"AI in Medicine\",\"Healthcare AI\",\"AI Medical Applications\",\"AI in Healthcare Innovations\",\"Machine Learning in Healthcare\",\"AI-driven Diagnostics\",\"AI in Patient Care\",\"AI-driven Healthcare Solutions\",\"AI in Healthcare Research\"]}" + } + } + ] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 124, + "completion_tokens": 50, + "total_tokens": 174 + }, + "system_fingerprint": "fp_400f27fa1f" + } + recorded_at: Mon, 22 Jul 2024 20:48:27 GMT +recorded_with: VCR 6.2.0 From e2e10edb133d4cb1cdb33adecee7ec0c646bf34b Mon Sep 17 00:00:00 2001 From: Scott Werner Date: Fri, 26 Jul 2024 16:23:29 -0400 Subject: [PATCH 2/2] Pend the gemini test to allow us to make other changes once we figure out why the endpoint is returning a 500 --- spec/generators/code_from_description_generator_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/generators/code_from_description_generator_spec.rb b/spec/generators/code_from_description_generator_spec.rb index b8a81ca..bd9f7fb 100644 --- a/spec/generators/code_from_description_generator_spec.rb +++ b/spec/generators/code_from_description_generator_spec.rb @@ -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"