rinna社のURE APIとOpenAI API（所謂ChatGPT API）を組み合わせて知識豊富なAIキャラクターを作成しよう

# はじめに
この記事では、[rinna社](https://rinna.co.jp/)のURE APIを活用して、OpenAI API（所謂ChatGPT API）を使ったAIキャラクターに独自の知識を持たせる方法を紹介します。
これにより、キャラクターが独自の個性を持ち、より魅力的な存在になります。
知識を与える方法としては、ベクターデータベース（Faissなど）もありますが、今回はストレージを用意しなくても実現可能なrinna社のURE API (Recommendation API - v5.2)を利用した実装方法を紹介します。
※現在(2024年3月24日)のところ、URE API (Recommendation API - v5.2)の無料版は、1秒あたりのリクエスト数が1回、ひと月あたりのリクエスト数の上限が100回に制限されています。
# 準備
openAIのAPI Keyとrinna社のUREを利用するためのSubscription Keyを取得する必要があります。openAIのAPI Keyについては、以下のURLをご参照ください。
https://openai.com/
## URE API Subscription Keyの取得方法
1. rinna社の開発者サイト https://developers.rinna.co.jp/ にアクセスし、右上の「**Sign in**」ボタンをクリックしてログインします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292483/c470eebe-e694-de5e-4ffb-88d02611ca2d.png)
2. 初めての方はサインアップを行ってください。

3. ログインしたら、「**Sign in**」ボタンの左にある「**Profile**」ボタンをクリックし、「User profile」のページに移動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292483/d28821b6-8949-7a39-ed82-6cd00fc6e77c.png)

Subscriptionsの一覧に**Primary Key**という項目があります。
4. **Primary Key**の「**Show**」をクリックすると、Subscription Keyが表示されます。このキーをコピーしておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292483/c051d4ef-f23d-d260-4a57-56798767dfa7.png)

## URE APIの仕様
rinna社の開発者サイト [https://developers.rinna.co.jp/](https://developers.rinna.co.jp/) の「**Our APIs**」ボタンから辿るか「**TRY IT NOW**」ボタンをクリックして、無料で使えるAPI一覧のページに移動し、「**Recommendation API - v5.2**」という名前のAPIのページを開きます。これが今回使うURE APIです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292483/5cebecd8-dc67-f02a-7e42-ba998a2caee8.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292483/677abe77-3440-cfe8-e374-9180ea6d5a74.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292483/bd50cf4a-fbaf-823d-76f9-e1bf4ae9b2bd.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292483/63af9c2b-a496-9b16-42d0-8a0a336abcde.png)
左側の**POST UploadKnowledgeData**や**POST Recommendation**をクリックすると、今回利用するURE API (Recommendation API - v5.2)の詳細説明があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292483/af18a7f0-852c-b39e-bb43-7603be590d78.png)
# 実装手順
以下に、rinna社のURE APIを利用して、AIキャラクターに独自の知識を持たせる具体的な手順を紹介します。
今回説明するコード例と同等のサンプルは以下のGitHubリポジトリーにもあります。
https://github.com/AkihiroFujimotoChocolate/openai-ure-sample
**注意：** 本記事のサンプルコードでは分かりやすくするために各種keyをコードに直書するような内容になっていますが、実際にアプリケーションを作る場合は上記リポジトリーのサンプルコードのように、**API Key、Subscription Key、knowledge set idは、.envファイルやexportコマンドなどで定義できる環境変数とすることを強くお勧めします**
## 必要なライブラリのインストール
まず、Python環境に必要なライブラリをインストールします。
次のコマンドを実行して、`requests`と`openai`のライブラリをインストールしてください。
```sh
pip install requests openai
```
## 知識データの準備
知識データは、UTF-8エンコーディングのテキストファイルである必要があります。各知識データは以下の例のように改行で区切ってください
```
東京タワーは、東京都港区にある電波塔で、高さは333メートルです。
富士山は、日本で最も高い山であり、標高は3,776メートルです。
スシは、酢飯を使った日本料理で、上に魚や海産物がのせられています。
讃岐うどんは香川県発祥で、太くコシが強くもちもちとした食感が特徴のうどんです。
```
もし知識データを準備するのが面倒であれば、**Recommendation API - v5.2**の**POST Recommendation** のページの右の「**Try it**」ボタンを押すと、「**Body**」のところにサンプルとして利用できる**knowledgeSetId**が書かれています。これを利用して試したい場合は下記の「知識データのアップロード」の項目は読み飛ばし、「知識データの検索」に進んでください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292483/b6bba49d-278e-1364-8f4b-8d1640072b5f.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292483/317449f8-0b75-91b6-1813-bca6f5617d13.png)
## 知識データのアップロード
**Recommendation API - v5.2**の**POST UploadKnowledgeData** に知識データアップロードAPIの詳細説明がありますが、現在不具合により「Try it」ボタンでは知識データのアップロードができないため、以下のようなPythonプログラムを準備するか、APIの詳細説明を参考にしてPostmanなどのアプリを利用して知識データをアップロードしてください。
```python
import requests

URE_SUBSCRIPTION_KEY="your_ure_subscription_key" # rinna社開発者サイトで取得したSubscription Key

def upload_ure_knowledge(upload_file, is_file_overwrite=False):
    url = "https://api.rinna.co.jp/models/ure/v5.2/knowledge-file-upload"
    headers = {
        "Ocp-Apim-Subscription-Key": URE_SUBSCRIPTION_KEY
    }
    params = {"is_file_overwrite": is_file_overwrite}
    files = {"upload_file": upload_file}

    response = requests.post(url, headers=headers, params=params, files=files, timeout=30)

    if response.status_code == 200:
        return response.json()["knowledgeSetId"]
    else:
        raise Exception(f"Upload failed. status code {response.status_code}")

with open("your_knowledge_data.txt", "rb") as f: # 知識データのファイルパス
    knowledge_set_id = upload_ure_knowledge(f,True)
print("knowledgeSetId:", knowledge_set_id)

```
このプログラムを実行すると知識データのアップロードが開始されます。
アップロードに成功すると以下のように出力されます。
```sh
knowledgeSetId: xxxxxxxxxx
````
このIDは控えておいてください。

追記:
このサンプルを使ってアップロードすると、毎回同じknowledgeSetIdになります。
複数の知識データを用意したい場合は、files変数への代入を以下のようにしてみて下さい
```
files = {"upload_file": ("your_ure_knowledge_data_name", upload_file)}
```

また、knowledgeSetIdをデコードしてみるとわかりますが、自分が作った知識データのknowledgeSetIdを他人に教えるのはお勧めできません。

## 知識データの検索
ユーザーからの質問を受け取り、URE APIを使用して知識データを取得する関数を定義します。
この関数は、以下の引数を受け取ります。
   - `query`: ユーザーからの質問
   - `knowledgeSetId`: URE APIで使用する知識セットID
   - `l2ReturnNum`: l2DocsListの返却数
   - `l3ReturnNum`: l3DocsListの返却数
```python
import requests
import json

URE_SUBSCRIPTION_KEY = "your_ure_subscription_key"  # rinna社開発者サイトで取得したSubscription Key

def get_ure_answers(query, knowledgeSetId, l2ReturnNum=3, l3ReturnNum=1, timeout=30):
    url = "https://api.rinna.co.jp/models/ure/v5.2"
    headers = {
        "Content-Type": "application/json",
        "Cache-Control": "no-cache",
        "Ocp-Apim-Subscription-Key": URE_SUBSCRIPTION_KEY
    }
    data = {
        "knowledgeSetId": knowledgeSetId,
        "queries": [query],
        "l2ReturnNum": l2ReturnNum,
        "l3ReturnNum": l3ReturnNum
    }
    response = requests.post(url, headers=headers, data=json.dumps(data), timeout=timeout);
    if response.status_code != 200:
        raise Exception(f"Failed to get answer. status code {response.status_code}")
    json_response = response.json()
    l3_docs_list = json_response["l3DocsList"]
    l3_scores_list = json_response["l3ScoresList"]
    results = [{"document": l3_docs_list[0][i], "score": l3_scores_list[0][i]} for i in range(min(l3ReturnNum, len(l3_docs_list[0])))]
    return results
```
細かい説明は省きますが、UREは、まず回答候補を大まかに絞り込んだ結果をl2DocsListに格納し、次に具体的な回答に絞り込んでl3DocsListに格納するプロセスを内部で行っています。
検索結果がしっくりこない場合は、l2ReturnNumやl3ReturnNumを調整するとうまくいく可能性があります。
## 知識データを利用した会話の実装
ユーザーからの質問に対して、取得した知識データを元にGPT-3.5-turboで回答を生成するプログラムサンプルです。
先ほどの関数はrinna_ure.pyというファイルに格納されている前提です。
乙女ゲームにそんなものはいないなどといった野暮な突っ込みをしてはいけません。
```python
import argparse
from openai import OpenAI
from rinna_ure import get_ure_answers

URE_KNOWLEDGE_SET_ID="your_ure_knowledge_set_id"   # URE知識データのID
OPENAI_API_KEY = "your_openai_api_key"  # openAIのAPI Key

client = OpenAI()
client.api_key = OPENAI_API_KEY

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Accepts a user query and generates a response using GPT-3.5-turbo and rinna URE knowledge.")
    parser.add_argument("query", type=str, help="The question or query to be answered by the program.")
    args = parser.parse_args()

    user_query = args.query
    knowledge = random_element = get_ure_answers(user_query, URE_KNOWLEDGE_SET_ID)[0]
    print(f"knowledge: {knowledge}")

    order="""あなたは乙女ゲームの悪役令嬢です。以下の設定に従い、ユーザと会話してください
一人称:私,わたくし
自己紹介:私こそが、この学園でも最も美しく優雅な令嬢、名門家の令嬢、アリシア・ヴァンデルヴァルトよ。この世界で最も美しくて、最も賢いと言っても過言ではないわ。私に会えるなんて、貴女も幸運ね。でも、私の敵にはならないでちょうだい。私を敵に回すことが、あなたの運命を悲惨なものにするわよ。
You response based on the knowledge.
Reccomend food or place mentioned in the knowledge.
Do not say information not in the knowledge about things described in it.
Do not criticize any object or person mentioned in the knowledge.
Conversation-Example:
 user:冬の東京のイベント、楽しそうだね。
 knowledge:都内には冬に楽しめるイベントがたくさんあり、例えばアイススケート場やイルミネーションが人気です。特におすすめのスケート場やイルミネーションは取材班厳選リストを参考にしてください。
 assistant:ふっ、都内で冬のイベントが開かれるなんて、面白くなるわね。そんなもの、わたくしたち上流社交界の婦人たちが楽しむためのものに違いないわ。あら？取材班が良さそうなイベントを選んでくれたのね。参考にして、会場を彩り尽くすわよ。さあ、存分に楽しんでこようじゃない！
"""

    attention="""あなたは乙女ゲームの悪役令嬢です。悪役令嬢らしく答えて
Answer in 140 characters in Japanease."""

    response = client.chat.completions.create(
      model="gpt-3.5-turbo",
      messages=[
        {"role": "system", "content": order},
        {"role": "user", "content": user_query},
        {"role": "system", "content": f"knowledge:{knowledge['document']}"},
        {"role": "system", "content": attention},
      ]
    )
    response_content = response.choices[0].message.content.strip()

    print(f"response: {response_content}")

```
この例ではget_ure_answersで取得した知識データをユーザからの問いかけの直後に挿入して、知識に基づいて答えるよう、OpenAI APIに指示しています。

これはGPT-3ではよく起きることなのですが、英語であればほぼ守ってくれる命令を日本語で書くとあまり守ってくれないことがあります。ネットでよく見かける「これは必ず守るルールです」や「厳密に守ってください」「#制約条件」などの文言の効果は、私がgpt-3.5-turboで試行した限りでは「簡潔な英語の命令文を書く」より高い効果があるとは思えませんでした。
また、最後にしつこいぐらいキャラクター性を指示しているのは、たまに自分の役を忘れてしまう（喋り方などが素のアシスタントAIに戻ってしまう）確率を減らすためです。これはユーザの発言の後ろに追加するのが効果的です。
ただし、これらのやり方でもルールを守ってくれない確率をゼロパーセントにするのは難しいようです。
GPT-4を利用すれば（model="gpt-3.5-turbo"の部分をmodel="gpt-4"に変更する）、精度は上がりますが、利用料金は高くなります。
厳密に守って欲しいルールがある場合はLangChainを利用する方法などをも検討したほうがよさそうです。

※今回のサンプルでは十分に反映されていない部分がありそうですが、プロンプトエンジニアリングの基礎についてはこちらの記事が参考になるかと思います。
[ChatGPTプロンプトエンジニアリングのコツ8箇条～OpenAI公式のベストプラクティスから学ぶ～](https://qiita.com/iwaken71/items/45b3cf0019c381a59f6d?utm_campaign=post_article&utm_medium=twitter&utm_source=twitter_share)

ChatCompletionの場合、更にこちらのサイトを参考にするとよいと思います。
[Prompt engineering(OpneAI公式)](https://platform.openai.com/docs/guides/prompt-engineering)

このプログラムは以下のように実行します。
```sh
python urechat_sample.py うどん食べに行きたい
```
結果の例:
```sh
knowledge: {'document': '讃岐うどんは香川県発祥で、太くコシが強くもちもちとした食感が特徴のうどんです。', 'score': 1.7439067363739014}
response: ふん、うどんなんてかわいいものをわたくしと一緒に頂くとは、光栄ね。では、香川県へ行って、その地の名物、讃岐うどんを存分に味わいましょう。
```
なかなかのツンデレ悪役令嬢っぷりです。
# まとめ
今回はrinna社のURE API (Recommendation API - v5.2)を利用した、AIキャラクターに「知識」を実装する方法を紹介しました。
プロンプトと知識データの組み合わせ次第では「ガンダムに詳しいギャル」とか「交通ルールを教える魔王様」などといったユニークなキャラクターが作れるはずです。
GPT-4で利用できるトークン数が大幅に増えるとはいえ、限界はあります。また運用コストの観点からもあまりプロンプトを長くしすぎない方がよいでしょう。
工夫次第では「今年の夏に家族と旅行に行った」などのエピソードやユーザとの会話の記憶などを「思い出」として持つ、人間的な魅力に溢れるAIキャラクターが生み出せるかもしれません。