GolangとGPT APIを利用して、入力したトピックから自動的にwordpressにブログを投稿できるようにしてみる

# はじめに
OpenAIからchatGPTという対話できるAIが世間を賑わせています。
これを利用して自動的にWordPressで記事を投稿してみようと思い、今勉強中のGolangを使って作ってみようと思いました。
GPT APIのドキュメントなどは、Pythonでのドキュメントがほとんどなので基本的にはPythonを利用するのが一般的かなと思います
ちなみにGPT APIの利用方法などは私が個人で書いてるブログで紹介してます

https://nbaboston.net/1824.html

また、全量のコードについては[こちら](https://github.com/tkwest3143/auto-blog-wordpress)に置いています。

https://github.com/tkwest3143/auto-blog-wordpress

### 参照
[GPT APIリファレンス](https://platform.openai.com/docs/api-reference/introduction)
[Wordpress APIリファレンス](https://developer.wordpress.org/rest-api/)

# まずはプロンプトを考える
質問した回答を想定して、回答は日本語で質問は英語にする必要があります。これは、APIの料金に関わってくるトークン数が日本語の方が多くなってしまうことから、質問に関しては英語で作成した方が良さそうです

まずは、結果を日本語で返してもらうために
```
You are a helpful assistant that speaks Japanese
```
とします。これをRoleのsystemにすることで会話が全て日本語で返されるようになりました

次に何をしてもらうかと条件をどうするか？ですが下記のようにすることでブログのタイトルと本文を考えてもらいます
``` 
#Operation.
Write a blog post for me. Generate a blog title and content for the topic: {topic}.
#conditions
 {conditions}
```
上記では{topic}にはブログのトピック、{conditions}にはブログの文字数などの条件を入力できるようにします

これで一旦プロンプトについては完了です
# 実装する
### 1. まずGPT APIの処理を作る
#### データ型を定義
```go 
type GptRequest struct {
	Model    string `json:"model"`
	Messages []struct {
		Role    string `json:"role"`
		Content string `json:"content"`
	} `json:"messages"`
}

type GptResponse struct {
	Usage struct {
		PromptTokens     int `json:"prompt_tokens"`
		CompletionTokens int `json:"completion_tokens"`
		TotalTokens      int `json:"total_tokens"`
	} `json:"usage"`
	Choices []struct {
		Message struct {
			Content string `json:"content"`
		} `json:"message"`
	} `json:"choices"`
}
```
上記では、APIにアクセスするためのデータ型を定義しています、文字列でもできると思いますがjsonをデータ型に変換したほうがわかりやすいのとgolangではjsonを変換する方法が簡単なので変換します
GptRequestのmodelには`gpt-4`などの使いたいモデル、Messagesは質問する内容になっています。[Role](https://platform.openai.com/docs/api-reference/chat/create#chat/create-role)についてはsystem, user, assistantとなっていてsystemとuserは同じ役割でAIへの質問内容になってますがGPT-4だと優先的にこちらに設定した質問が適応されるようです。
assistantについては質問する内容に補足する内容を設定するようです
#### APIへのアクセス
``` go
func GetContent(topic string, apiKey string) (string, string, error) {
	url := "https://api.openai.com/v1/chat/completions"
    prompt := "#Operation.\n Write a blog post for me. Generate a blog title and content for the topic: {topic}.\n#conditions \n {conditions}"

	fmt.Println("---------- this prompt create ----------")
	
	conditions := strings.Join(["At least 100 words of text"], "\n")
	prompt = strings.ReplaceAll(prompt, "{topic}", topic)
	prompt = strings.ReplaceAll(prompt, "{conditions}", conditions)
	fmt.Println(prompt)
	fmt.Println("---------- prompt end  ----------")
	payload := &GptRequest{
		Model: "gpt-4",
		Messages: []struct {
			Role    string `json:"role"`
			Content string `json:"content"`
		}{
			{
				Role:    "system",
				Content: "You are a helpful assistant that speaks Japanese",
			},
			{
				Role:    "user",
				Content: prompt,
			},
		},
	}

	jsonPayload, err := json.Marshal(payload)
	if err != nil {
		return "", "", err
	}

	req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonPayload))
	if err != nil {
		return "", "", err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey))

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", "", err
	}
	fmt.Println("http status " + resp.Status)

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", "", err
	}

	var gpt4Response GptResponse
	err = json.Unmarshal(body, &gpt4Response)
	if err != nil {
		return "", "", err
	}
	text := gpt4Response.Choices[0].Message.Content
	split := strings.SplitN(text, "\n", 2)
	title := split[0]
	content := ""
	if len(split) > 1 {
		content = split[1]
	}
	fmt.Println("usage PromptTokens =", gpt4Response.Usage.PromptTokens)
	fmt.Println("usage CompletionTokens =", gpt4Response.Usage.CompletionTokens)
	fmt.Println("usage TotalTokens =", gpt4Response.Usage.TotalTokens)
	return title, content, nil
}
```
上記のコードでは、引数に指定されたトピックとAPI keyからプロンプトを作成して、GPT APIへのPOST送信を行い、帰ってきたレスポンスのjsonデータをGptResponse型に変換した後に、タイトルと本文を切り出し、戻り値にしています。
リクエストとして、jsonデータへの変換が`
	jsonData, _ := json.Marshal(payload)`、レスポンスをデータ型に変換するのがjson.Unmarshal(body, &gpt4Response)のように１発でできるのでとても簡単です

### 2. wordpress側の処理を作る
#### データ型の定義
GPTと同様にリクエスト、レスポンスのデータ型を作ります
```go
type WPRequest struct {
	Title   string `json:"title"`
	Content string `json:"content"`
	Status  string `json:"status"`
}

type WPResponse struct {
	ID int `json:"id"`
}
```
今回必要になってくるのはタイトル、本文、公開するか否か？なのでリクエストにはTitle,Content,Statusの３つを定義して、レスポンスには作成した記事のIDを取得できるようにしました

#### APIへのアクセス
```go
func PostBlog(title string, content string, siteName string, apiKey string, postUserName string) error {
	url := siteName + "/wp-json/wp/v2/posts"
	payload := &WPRequest{
		Title:   title,
		Content: content,
		Status:  "publish",
	}
	jsonPayload, err := json.Marshal(payload)
	if err != nil {
		return err
	}

	req, _ := http.NewRequest("POST", url, bytes.NewBuffer(jsonPayload))

	req.Header.Add("Content-Type", "application/json")

	req.SetBasicAuth(postUserName, apiKey)

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}

	fmt.Println("http status " + resp.Status)

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	var wpResponse WPResponse
	json.Unmarshal(body, &wpResponse)

	fmt.Printf("Posted new blog with ID: %d\n", wpResponse.ID)
	return nil
}
```
上記では引数に指定されたタイトル、本文、サイト名（ドメイン名）、APIkey、作成ユーザ名をもとに記事を投稿します
今回はStatusをpublishにすることで公開するようにします

### 3. main.goの作成
最後に作成した関数を使って実際に投稿するためにエントリーポイントとなるmain.goを作成します
```go
func main() {
	openAIKey := os.Getenv("OPENAI_API_KEY")
	siteName := os.Getenv("WORDPRESS_SITE_NAME")
	postUserName := os.Getenv("WORDPRESS_USER_NAME")
	wordpressKey := os.Getenv("WORDPRESS_API_KEY")

	reader := bufio.NewReader(os.Stdin)
	fmt.Println("Enter the topic for the blog post:")
	topic, _ := reader.ReadString('\n')

	fmt.Println("Thank you input topic")
	topicLog := fmt.Sprintf("generating blog for gpt [topic = %s\n]", topic)
	fmt.Println(topicLog)
	title, content, err := openai.GetContent(topic, openAIKey)
	fmt.Println("completing generating blog for gpt")
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println("posting blog")
	wordpress.PostBlog(title, content, siteName, wordpressKey, postUserName)
	fmt.Println("complete posting blog")
}
```
上記コードでは、`os.Getenv`を用いて環境変数からAPIキーやサイト名など必要な情報を取得し、`reader := bufio.NewReader(os.Stdin)`を用いて、CLIから入力するトピック情報をもとに記事を作成していきます。

### 4. 実行
今回は環境変数を利用したいので.envファイルを作成して、makefileで実行してみたいと思います
```env
OPENAI_API_KEY=your_openai_api_key_here
WORDPRESS_SITE_NAME=your_wordpress_site_name_here
WORDPRESS_USER_NAME=your_wordpress_user_name_here
WORDPRESS_API_KEY=your_wordpress_api_key_here
```
```makefile
include .env
export

.PHONY: build run

build:
	go build -o build/auto-blog-wordpress

run: build
	./build/auto-blog-wordpress
```

上記で`make run`コマンドを実行してみます
```bash
make run
go build -o build/auto-blog-wordpress
./build/auto-blog-wordpress
Enter the topic for the blog post:
sample
Thank you input topic
generating blog for gpt topic = sample

---------- the topics ----------
generating blog for gpt [topic = sample
]
---------- end  ----------
---------- this prompt create ----------
#Operation.
 Write a blog post for me. Generate a blog title and content for the topic: sample
.
#conditions 
 content is 'sample'
---------- prompt end  ----------
http status 200 OK
usage PromptTokens = 50
usage CompletionTokens = 882
usage TotalTokens = 932
completing generating blog for gpt
posting blog
http status 201 Created
Posted new blog with ID: 2152
complete posting blog
```
ちょっとログがおかしいような感じがしますが、成功しました。
wordpressを見ても、それらしい内容の記事が作成できていました。

ただ、帰ってくる内容がhtmlではないため、ソースコードや段落の指定などまだまだなのでプロンプトの調整や、wordpressの設定でなんとかして、いい感じに記事が投稿できるようにしてきたいと思います