🐈

Redmine で課題が起票された際に Azure OpenAI Service を利用して自動返信する

2023/10/22に公開

TL;DR

  • Redmine で課題起票したときに Azure OpenAI Service が返事返してくれたら便利そう
  • Redmine には plugin の仕組みがあるので割と簡単に実装できそう
  • ChatGPT にめっちゃお世話になりながらとりあえず動くものができた

plugin のひな形を作成する

ikko@vm-rdn01:/var/lib/redmine$ sudo -u www-data RAILS_ENV=production bundle exec rails generate redmine_plugin openai
      create  plugins/openai/app
      create  plugins/openai/app/controllers
      create  plugins/openai/app/helpers
      create  plugins/openai/app/models
      create  plugins/openai/app/views
      create  plugins/openai/db/migrate
      create  plugins/openai/lib/tasks
      create  plugins/openai/assets/images
      create  plugins/openai/assets/javascripts
      create  plugins/openai/assets/stylesheets
      create  plugins/openai/config/locales
      create  plugins/openai/test
      create  plugins/openai/test/fixtures
      create  plugins/openai/test/unit
      create  plugins/openai/test/functional
      create  plugins/openai/test/integration
      create  plugins/openai/test/system
      create  plugins/openai/README.rdoc
      create  plugins/openai/init.rb
      create  plugins/openai/config/routes.rb
      create  plugins/openai/config/locales/en.yml
      create  plugins/openai/test/test_helper.rb

plugin の概要を記述する

ファイル自体は rails generate redmine_plugin で作成されているのですが、settings から始まる行を追加して Web から設定が変更できるようにします。

ikko@vm-rdn01:/var/lib/redmine$ cat /var/lib/redmine/plugins/openai/init.rb
init.rb
Redmine::Plugin.register :openai do
  name 'Openai plugin'
  author 'Author name'
  description 'This is a plugin for Redmine'
  version '0.0.1'
  url 'http://example.com/path/to/plugin'
  author_url 'http://example.com/about'

  settings default: {'api_url' => '', 'api_key' => ''}, partial: 'settings/openai_settings'
end

plugin の設定画面を作成する

該当のフォルダとファイルはないので新規作成します。
root 権限で作業する場合には owner に注意してください。

ikko@vm-rdn01:/var/lib/redmine$ cat /var/lib/redmine/plugins/openai/app/views/settings/_openai_settings.html.erb
_openai_settings.html.erb
<p>
  <label for="your_plugin_name_api_url">ChatGPT API URL:</label>
  <%= text_field_tag 'settings[api_url]', Setting.plugin_openai['api_url'], :size => 40 %>
</p>
<p>
  <label for="your_plugin_name_api_key">ChatGPT API Key:</label>
  <%= text_field_tag 'settings[api_key]', Setting.plugin_openai['api_key'], :size => 40 %>
</p>
<p>
  <label for="your_plugin_name_system_prompt">System Prompt:</label><br>
  <%= text_area_tag 'settings[system_prompt]', Setting.plugin_openai['system_prompt'], :rows => 5, :cols => 60 %>
</p>

課題が起票された際に Azure OpenAI Service の API を呼び出す hook を記述する

こちらも同じく、該当のフォルダとファイルはないので新規作成します。
root 権限で作業する場合には owner に注意してください。

ikko@vm-rdn01:/var/lib/redmine$ cat /var/lib/redmine/plugins/openai/lib/openai/hooks.rb
hooks.rb
module Openai
  class Hooks < Redmine::Hook::ViewListener
    def controller_issues_new_after_save(context = {})
      issue = context[:issue]

      # Call the ChatGPT API and retrieve the response
      response = call_chatgpt_api(issue)

      # Add the response as a comment to the Issue
      Journal.create!(
        journalized_id: issue.id,
        journalized_type: 'Issue',
        user_id: User.find_by(login: 'chatbot').id, # Use the user with login 'chatbot'
        notes: response
      )
    end

    private

    def call_chatgpt_api(issue)
      require 'net/http'
      require 'json'

      # Retrieve the settings
      api_base_url = "https://#{Setting.plugin_openai['api_url']}"
      api_path = "/openai/deployments/gpt-35-turbo/chat/completions?api-version=2023-07-01-preview"
      api_key = Setting.plugin_openai['api_key']
      system_prompt = Setting.plugin_openai['system_prompt']

      # Create a conversational structure for the API request
      messages = [
        {
          "role": "system",
          "content": system_prompt
        },
        {
          "role": "user",
          "content": "Issue: #{issue.subject}\nDescription: #{issue.description}"
        }
      ]

      # Set up the API request details
      uri = URI(api_base_url + api_path)
      request = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json', 'api-key' => api_key)
      request.body = {
        "messages": messages,
        "max_tokens": 800,
        "temperature": 0.7,
        "frequency_penalty": 0,
        "presence_penalty": 0,
        "top_p": 0.95,
        "stop": nil
      }.to_json

      # Execute the API request
      response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
        http.request(request)
      end

      # Log the response
      Rails.logger.info("ChatGPT API Response: #{response.body}")

      # Parse the response and return the bot's message
      result = JSON.parse(response.body)
      if result['choices']
        result['choices'].first['message']['content'].strip
      else
        Rails.logger.warn("Unexpected response format from ChatGPT API")
        ""
      end
    end
  end
end

require_dependency はいらない

ChatGPT を使いながら作成しており、その中で require_dependency 'openai/hooks' を init.rb に書くべしと回答が来ていたのですが、Redmine 5.0 (というよりかは Zeitwerk) の仕様により、不要になっているようです。
GitHub - fxn/zeitwerk: Efficient and thread-safe code loader for Ruby とか Zeitwerkの壊し方 - Qiita などからそうだと判断しています。

参考

  • GitHub - fxn/zeitwerk: Efficient and thread-safe code loader for Ruby

https://github.com/fxn/zeitwerk

  • Zeitwerkの壊し方 - Qiita

https://qiita.com/fursich/items/717a720d9f4465e4cbbb

  • マネージド サービスの Redmine に対して Azure OpenAI Service を利用して自動返信する

https://zenn.dev/skmkzyk/articles/openai-redmine-external

Discussion