Emacsの対話セラピー機能doctorをChatGPTに対応させる

https://res.cloudinary.com/symdon/image/upload/v1673760769/blog.symdon.info/1673355987/doctor4_plnppp.gif
できあがり

最近の機械学習の進歩により、自然言語を生成する機能が劇的に進化しているように思う。例えばチャットボットは、英語や日本語等の自然言語を使用して対話をする機能のことで、最近はウェブページの左下にチャットボットとの対話インターフェースが埋め込まれているのをよく目にする。ユーザーはそのチャットボットに英語や日本語で質問し、サービスの使い方を知ったり問題を解決するというような使い方をする事が多い。特にChatGPTというサービスが高精度な回答をするということで話題となっていて、様々な使用方法が提案されている。本当に進化している。

一方でEmacsは対話形式のセラピーを提供するdoctorという機能を古くから梱包している。これは心理カウンセラーのような対話をする"人工知能"で、会話の最後には「秘書が請求書を送る」といったメッセージをくれる1。まさに最近チャットボットと呼ばれる機能であり、Emacsのこの機能はELIZAと呼ばれるようだ2

内部はEmacs Lispで実装されており、実際にdoctor.elを読んでみると内部に doctor-eliza という記述がある。また doctor-rms というのもある3。簡単なパターンマッチによって実装されているため、現代の水準からすると会話の精度はイマイチだし、日本語は使えない。EmacsのELIZAは時を止めたまま、ひっそりと存在し続けている。

ただよく考えてみると、Emacsの考え方の一つに外の世界と協調するというものがあるように思う。外部プロセスとして実行できるものは、Emacs内部で抱え込まず、外に追い出すというものだ。ChatGPTはベータではあるがAPIを提供している。つまりELIZAをChatGPTに対応させられれば劇的に機能改善できる。そこで、ChatGPTを利用し、EmacsのELIZAをChatGPTに対応させることにした。

やったことは単純で、OpenAIが提供しているChatGPTのAPIにリクエストを送信し、レスポンスを受け取りdoctorのバッファに表示している。doctorはEmacs Lispで実装されている。本当は解析の部分と文章の生成のみを置き換えたかったが、doctor.elを読んでみると、それはそれで手間がかかる作業に思えた。そこで文字の読み込みから、出力までを行っている doctor-read-print 関数を、ChatGPT用の関数 doctor-chatgpt-read-print に置き換える事でお茶を濁すことにした。良い方法とは思わないが、想定されていない方法であってもやりたい事を実現できる、この柔軟性がEmacsの良い所だと思う。以下にソースコードを置いておく。

doctor-chatgpt.el

;;; doctor-chatgpt --- Talk to doctor(backend OpenAI ChatGPT)  -*- lexical-binding: t -*-

;; Copyright (C) 2023 TakesxiSximada

;; Author: TakesxiSximada <[email protected]>
;; Maintainer: TakesxiSximada <[email protected]>
;; Repository:
;; Version: 2
;; Package-Version: 20230409.0000
;; Package-Requires: ((emacs "28.0") (plz "0.3") (plz "0.3") (json "1.5"))
;; Date: 2023-04-09

;; This file is part of doctor-chatgpt.

;; doctor-chatgpt is free software: you can redistribute it and/or
;; modify it under the terms of the GNU Affero General Public License as
;; published by the Free Software Foundation, either version 3 of the
;; License, or (at your option) any later version.

;; doctor-chatgpt is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
;; Affero General Public License for more details.

;; You should have received a copy of the GNU Affero General Public
;; License along with this program.  If not, see
;; <https://www.gnu.org/licenses/>.

;;; Code:

(require 'plz)
(require 'json)
(require 'doctor)

(defvar doctor-current-answer-marker (make-marker))
(defvar doctor-current-talk-end-marker (make-marker))

(defvar doctor-chatgpt-buffer-name "*doctor*"
  "name of doctor buffer")

(defcustom doctor-chatgpt-api-origin "https://api.openai.com"
  "API Origin of OpenAI ChatGPT")

(defcustom doctor-chatgpt-access-token nil
  "API access token of OpenAI ChatGPT")

(defun doctor-chatgpt-api-get-url (path)
  (concat doctor-chatgpt-api-origin path))

;;;###autoload
(defun doctor-chatgpt-activate ()
  (interactive)
  (define-key doctor-mode-map (kbd "C-j") nil)
  (define-key doctor-mode-map (kbd "C-c C-c") #'doctor-read-print))
  (define-key doctor-mode-map (kbd "RET") nil)

(defun doctor-chatgpt-parse-response-get-text (data)
  "ChatGPT Response parser"
  (cdr (assoc 'text (elt (cdr (assoc 'choices data)) 0))))

;; backup original function
(unless (symbol-function 'doctor-read-print-original)
  (defalias 'doctor-read-print-original (symbol-function #'doctor-read-print)))

(unless (symbol-function 'doctor-readin-original)
  (defalias 'doctor-readin-original (symbol-function #'doctor-readin)))

(unless (symbol-function 'doctor-doc-original)
  (defalias 'doctor-doc-original (symbol-function #'doctor-doc)))

;; extend doctor functions
(defun doctor-readin-all-sentences ()
  (interactive)
  (list
   (buffer-substring-no-properties
    (or (marker-position doctor-current-talk-end-marker) 120)
    (point-max))))

(defun doctor-read-print-extra ()
  "Top level loop."
  (interactive nil doctor-mode)
  (setq doctor-sent (doctor-readin))
  (insert (if (equal (line-beginning-position) (point))
    "\n" "\n\n"))
  (insert "#+begin_example\n")
  (set-marker doctor-current-answer-marker (point))
  (goto-char (+ (point) 1))
  (insert "#+end_example\n\n")
  (set-marker doctor-current-talk-end-marker (point))
  (doctor-doc))

(defun doctor-doc-chatgpt ()
  (let* ((sentence (car doctor-sent))
         (endpoint (doctor-chatgpt-api-get-url "/v1/completions"))
         (headers `(("Content-Type" . "application/json")
                    ("Authorization" . ,(format "Bearer %s" doctor-chatgpt-access-token))))
         (body (json-encode `(("model" . "text-davinci-003")
                              ("prompt" . ,sentence)
                              ("temperature" . 0.9)
                              ("max_tokens" . 1000)
                              ("top_p" . 1)
                              ("frequency_penalty" . 0.0)
                              ("presence_penalty" . 0.6)
                              ("stop" . (" Human:" " AI:"))))))
    (plz 'post endpoint :headers headers :body body
      :as #'json-read
      :then (lambda (d)
              (let ((answer (doctor-chatgpt-parse-response-get-text d)))
                (with-current-buffer (marker-buffer doctor-current-answer-marker)
                  (save-excursion
                    (goto-char (marker-position doctor-current-talk-end-marker))
                    (insert answer)))))
      :else (lambda (d)
        (princ d)))))

(defalias 'doctor-readin (symbol-function #'doctor-readin-all-sentences))
(defalias 'doctor-read-print (symbol-function #'doctor-read-print-extra))
(defalias 'doctor-doc (symbol-function #'doctor-doc-chatgpt))

(defun doctor-chatgpt-recovery ()
  (interactive)
  (defalias 'doctor-read-print (symbol-function #'doctor-read-print-original))
  (defalias 'doctor-readin (symbol-function #'doctor-readin-original))
  (defalias 'doctor-doc (symbol-function #'doctor-doc-original)))

(provide 'doctor-chatgpt)
;;; doctor-chatgpt ends here

1

もちろん実際に金銭を請求されるわけではない。