-
Notifications
You must be signed in to change notification settings - Fork 9
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
Introduce OpenAI action #621
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
d4109b2
Introduce OpenAI action
AliSoftware 5a6fd7e
Refactor predefined prompts (allow Symbols)
AliSoftware 96fe170
Improve parsing logic
AliSoftware 942813d
Add example `Fastfile` call as part of code doc
AliSoftware 9d0e291
Add CHANGELOG entry
AliSoftware b0e55f0
Fix `s/OpenAPI/OpenAI/` typos
AliSoftware 11a7b37
Fix typo
AliSoftware c829b8d
Rename action to `openai_ask`
AliSoftware File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
141 changes: 141 additions & 0 deletions
141
lib/fastlane/plugin/wpmreleasetoolkit/actions/common/openai_ask_action.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
require 'fastlane/action' | ||
require 'net/http' | ||
require 'json' | ||
|
||
module Fastlane | ||
module Actions | ||
class OpenaiAskAction < Action | ||
OPENAI_API_ENDPOINT = URI('https://api.openai.com/v1/chat/completions').freeze | ||
|
||
PREDEFINED_PROMPTS = { | ||
release_notes: <<~PROMPT.freeze | ||
Act like a mobile app marketer who wants to prepare release notes for Google Play and App Store. | ||
Do not write it point by point and keep it under 350 characters. It should be a unique paragraph. | ||
|
||
When provided a list, use the number of any potential "*" in brackets at the start of each item as indicator of importance. | ||
Ignore items starting with "[Internal]", and ignore links to GitHub. | ||
PROMPT | ||
}.freeze | ||
|
||
def self.run(params) | ||
api_token = params[:api_token] | ||
prompt = params[:prompt] | ||
prompt = PREDEFINED_PROMPTS[prompt] if PREDEFINED_PROMPTS.key?(prompt) | ||
question = params[:question] | ||
|
||
headers = { | ||
'Content-Type': 'application/json', | ||
Authorization: "Bearer #{api_token}" | ||
} | ||
body = request_body(prompt: prompt, question: question) | ||
|
||
response = Net::HTTP.post(OPENAI_API_ENDPOINT, body, headers) | ||
|
||
case response | ||
when Net::HTTPOK | ||
json = JSON.parse(response.body) | ||
json['choices']&.first&.dig('message', 'content') | ||
else | ||
UI.user_error!("Error in OpenAI API response: #{response}. #{response.body}") | ||
end | ||
end | ||
|
||
def self.request_body(prompt:, question:) | ||
{ | ||
model: 'gpt-4o', | ||
response_format: { type: 'text' }, | ||
temperature: 1, | ||
max_tokens: 2048, | ||
top_p: 1, | ||
messages: [ | ||
format_message(role: 'system', text: prompt), | ||
format_message(role: 'user', text: question), | ||
].compact | ||
}.to_json | ||
end | ||
|
||
def self.format_message(role:, text:) | ||
return nil if text.nil? || text.empty? | ||
|
||
{ | ||
role: role, | ||
content: [{ type: 'text', text: text }] | ||
} | ||
end | ||
|
||
##################################################### | ||
# @!group Documentation | ||
##################################################### | ||
|
||
def self.description | ||
'Use OpenAI API to generate response to a prompt' | ||
end | ||
|
||
def self.authors | ||
['Automattic'] | ||
end | ||
|
||
def self.return_value | ||
'The response text from the prompt as returned by OpenAI API' | ||
end | ||
|
||
def self.details | ||
<<~DETAILS | ||
Uses the OpenAI API to generate response to a prompt. | ||
Can be used to e.g. ask it to generate Release Notes based on a bullet point technical changelog or similar. | ||
DETAILS | ||
end | ||
|
||
def self.examples | ||
[ | ||
<<~EXEMPLE, | ||
items = extract_release_notes_for_version(version: app_version, release_notes_file_path: 'RELEASE-NOTES.txt') | ||
nice_changelog = openai_ask( | ||
prompt: :release_notes, # Uses the pre-crafted prompt for App Store / Play Store release notes | ||
question: "Help me write release notes for the following items:\n#{items}", | ||
api_token: get_required_env('OPENAI_API_TOKEN') | ||
) | ||
File.write(File.join('fastlane', 'metadata', 'android', 'en-US', 'changelogs', 'default.txt'), nice_changelog) | ||
EXEMPLE | ||
] | ||
end | ||
|
||
def self.available_prompt_symbols | ||
PREDEFINED_PROMPTS.keys.map { |v| "`:#{v}`" }.join(',') | ||
end | ||
|
||
def self.available_options | ||
[ | ||
FastlaneCore::ConfigItem.new(key: :prompt, | ||
description: 'The internal top-level instructions to give to the model to tell it how to behave. ' \ | ||
+ "Use a Ruby Symbol from one of [#{available_prompt_symbols}] to use a predefined prompt instead of writing your own", | ||
optional: true, | ||
default_value: nil, | ||
type: String, | ||
skip_type_validation: true, | ||
verify_block: proc do |value| | ||
next if value.is_a?(String) | ||
next if PREDEFINED_PROMPTS.include?(value) | ||
|
||
UI.user_error!("Parameter `prompt` can only be a String or one of the following Symbols: [#{available_prompt_symbols}]") | ||
end), | ||
FastlaneCore::ConfigItem.new(key: :question, | ||
description: 'The user message to ask the question to the OpenAI model', | ||
optional: false, | ||
default_value: nil, | ||
type: String), | ||
FastlaneCore::ConfigItem.new(key: :api_token, | ||
description: 'The OpenAI API Token to use for the request', | ||
env_name: 'OPENAI_API_TOKEN', | ||
optional: false, | ||
sensitive: true, | ||
type: String), | ||
] | ||
end | ||
|
||
def self.is_supported?(_platform) | ||
true | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
require 'spec_helper' | ||
|
||
describe Fastlane::Actions::OpenaiAskAction do | ||
let(:fake_token) { 'sk-proj-faketok' } | ||
let(:endpoint) { Fastlane::Actions::OpenaiAskAction::OPENAI_API_ENDPOINT } | ||
|
||
def stubbed_response(text) | ||
<<~JSON | ||
{ | ||
"id": "chatcmpl-Aa2NPY4sSWF5eKoW1aFBJmfc78y9p", | ||
"object": "chat.completion", | ||
"created": 1733152307, | ||
"model": "gpt-4o-2024-08-06", | ||
"choices": [ | ||
{ | ||
"index": 0, | ||
"message": { | ||
"role": "assistant", | ||
"content": #{text.to_json}, | ||
"refusal": null | ||
}, | ||
"logprobs": null, | ||
"finish_reason": "stop" | ||
} | ||
], | ||
"usage": { | ||
"prompt_tokens": 91, | ||
"completion_tokens": 68, | ||
"total_tokens": 159, | ||
"prompt_tokens_details": { | ||
"cached_tokens": 0, | ||
"audio_tokens": 0 | ||
}, | ||
"completion_tokens_details": { | ||
"reasoning_tokens": 0, | ||
"audio_tokens": 0, | ||
"accepted_prediction_tokens": 0, | ||
"rejected_prediction_tokens": 0 | ||
} | ||
}, | ||
"system_fingerprint": "fp_831e067d82" | ||
} | ||
JSON | ||
end | ||
|
||
def run_test(prompt_param:, question_param:, expected_prompt:, expected_response:) | ||
expected_req_body = described_class.request_body(prompt: expected_prompt, question: question_param) | ||
|
||
stub = stub_request(:post, endpoint) | ||
.with(body: expected_req_body) | ||
.to_return(status: 200, body: stubbed_response(expected_response)) | ||
|
||
result = run_described_fastlane_action( | ||
api_token: fake_token, | ||
prompt: prompt_param, | ||
question: question_param | ||
) | ||
|
||
# Ensure the body of the request contains the expected JSON data | ||
messages = JSON.parse(expected_req_body)['messages'] | ||
if expected_prompt.nil? || expected_prompt.empty? | ||
expect(messages.length).to eq(1) | ||
expect(messages[0]['role']).to eq('user') | ||
expect(messages[0]['content']).to eq(['type' => 'text', 'text' => question_param]) | ||
else | ||
expect(messages.length).to eq(2) | ||
expect(messages[0]['role']).to eq('system') | ||
expect(messages[0]['content']).to eq(['type' => 'text', 'text' => expected_prompt]) | ||
expect(messages[1]['role']).to eq('user') | ||
expect(messages[1]['content']).to eq(['type' => 'text', 'text' => question_param]) | ||
end | ||
|
||
# Ensure the request has been made and return the action response for it to be validated in calling test | ||
expect(stub).to have_been_requested | ||
result | ||
end | ||
|
||
it 'calls the API with no prompt' do | ||
result = run_test( | ||
prompt_param: '', | ||
question_param: 'Say Hi', | ||
expected_prompt: nil, | ||
expected_response: 'Hello! How can I assist you today?' | ||
) | ||
|
||
expect(result).to eq('Hello! How can I assist you today?') | ||
end | ||
|
||
it 'calls the API with :release_notes prompt' do | ||
changelog = <<~CHANGELOG | ||
- [Internal] Fetch remote FF on site change [https://github.com/woocommerce/woocommerce-android/pull/12751] | ||
- [**] Improve barcode scanner reading accuracy [https://github.com/woocommerce/woocommerce-android/pull/12673] | ||
- [Internal] AI product creation banner is removed [https://github.com/woocommerce/woocommerce-android/pull/12705] | ||
- [*] [Login] Fix an issue where the app doesn't show the correct error screen when application passwords are disabled [https://github.com/woocommerce/woocommerce-android/pull/12717] | ||
- [**] Fixed bug with coupons disappearing from the order creation screen unexpectedly [https://github.com/woocommerce/woocommerce-android/pull/12724] | ||
- [Internal] Fixes crash [https://github.com/woocommerce/woocommerce-android/issues/12715] | ||
- [*] Fixed incorrect instructions on "What is Tap to Pay" screen in the Payments section [https://github.com/woocommerce/woocommerce-android/pull/12709] | ||
- [***] Merchants can now view and edit custom fields of their products and orders from the app [https://github.com/woocommerce/woocommerce-android/issues/12207] | ||
- [*] Fix size of the whats new announcement dialog [https://github.com/woocommerce/woocommerce-android/pull/12692] | ||
- [*] Enables Blaze survey [https://github.com/woocommerce/woocommerce-android/pull/12761] | ||
CHANGELOG | ||
|
||
expected_response = <<~TEXT | ||
Exciting updates are here! We've enhanced the barcode scanner for optimal accuracy and resolved the issue with coupons vanishing during order creation. Most significantly, merchants can now effortlessly view and edit custom fields for products and orders directly within the app. Additionally, we've improved error handling on login and fixed various UI inconsistencies. Enjoy a smoother experience! | ||
TEXT | ||
|
||
result = run_test( | ||
prompt_param: :release_notes, | ||
question_param: "Help me write release notes for the following items:\n#{changelog}", | ||
expected_prompt: Fastlane::Actions::OpenaiAskAction::PREDEFINED_PROMPTS[:release_notes], | ||
expected_response: expected_response | ||
) | ||
|
||
expect(result).to eq(expected_response) | ||
end | ||
end |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI: I forgot to update the CHANGELOG before merging this PR, but I've fixed it directly in
trunk
afterwards, via 1a58ba0