OCI Generative AI の Cohere command R / R + でFunction Callingしてみた

## はじめに
ついにOCI Generative AI の Cohere command R + が使用できるようになりました！！
CohereのFunction Callingを使うと生成されたテキストやデータに対して追加の処理や特定のアクションを行うことができます。
今回は[こちらの記事](https://qiita.com/shukawam/items/14ee6efc69f75ace0a0e)を応用してOCI Generative AI の 
**Cohere command R/R +を使ってFunction Callingを試す＋回答の違い**
を見比べていきたいと思います。

シナリオは
- 質問の郵便番号から緯度と経度を算出する
- Function Callingを使って
    - 郵便番号から住所を割り出し※[郵便番号API](https://github.com/ttskch/jp-postal-code-api)を使用
    - 住所から緯度と経度を算出※[国土地理院API](https://vldb.gsi.go.jp/sokuchi/surveycalc/api_help.html)を使用
を内部的に行います。


## OCI Generative AI について
OCI Generative AI（Oracle Cloud Infrastructure Generative AI）は、OCIのSaaSとしてお使いいただける生成AI（Generative AI）サービスの一部です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3763355/6a21ef5b-301d-4d15-ead6-24ab0992e7a2.png)

OCI Generative AIサービスの[ファンデーションモデル（事前学習済みモデル）](https://docs.oracle.com/ja-jp/iaas/Content/generative-ai/pretrained-models.htm#pretrained-models)では、
- チャットモデル：会話型回答を行うモデル
    - cohere.command R +
    - cohere.command R
    - meta.llama-3

が提供されており、今回はこちらの Cohere command R/R+を使用します。

その他にも事前学習済みモデルとして、
- テキスト生成モデル：テキストを生成したり、テキストから情報を抽出するモデル
- テキスト要約モデル：指示した形式でテキストを要約するモデル
- Embed（埋め込み）モデル：ベクトル変換を行うモデル

を使用することができます。
## Cohere command R / R +とは
Command RとCommand R+はCohere社が開発したエンタープライズ向けのLLMです。
R / R +とも日本語も含め10カ国語対応しています。
[Cohereのドキュメント](https://docs.cohere.com/docs/command-r-plus)には、
- Command R+：複雑なRAGや複数ステップのツール使用するワークフローに向く
- Command R：単純なRAGや単一ステップのツール使用と価格が重要な考慮事項となるアプリケーションに最適

とあります。
特に Command R +は、[Cohere Blog](https://cohere.com/blog/command-r-plus-microsoft-azure)にてGPT4-turboと性能を比較したベンチマークもあり、低コストで高い精度を実現したモデルといえます。

## 実装してみた
### 環境
今回はこちらの環境で構築していきます。
- Python 3.12.2

### 1. 必要なライブラリを設定

```Python
import oci

from oci.auth.signers import InstancePrincipalsSecurityTokenSigner
from oci.generative_ai_inference.generative_ai_inference_client import GenerativeAiInferenceClient
from oci.generative_ai_inference.models import (
    ChatDetails,
    OnDemandServingMode,
    CohereChatRequest,
    CohereTool,
    CohereParameterDefinition,
    CohereToolCall,
    CohereToolResult
)
```
### 2. OCI認証設定を行う
```import os```で環境変数からcompartment_idやendpointを指定していただいても問題ないです。
```Python
compartment_id ="ocid1.compartment.***********************************"
endpoint = "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com"
config = oci.config.from_file(file_location='~/.oci/config', profile_name="CHICAGO")
signer = InstancePrincipalsSecurityTokenSigner()
## AIクライアント作成
generative_ai_inference_client = GenerativeAiInferenceClient(config = {}, signer = signer, service_endpoint = endpoint)
```
### 3. まずはcommmand R /R+単体での回答を見る
```model_id```を変えてそれぞれ回答を見てみます。
- command Rの場合：cohere.command-r-16k v1.2
- command R +の場合：cohere.command-r-plus

と、モデルを指定しています。

```Python
response = generative_ai_inference_client.chat(
    chat_details=ChatDetails(
        compartment_id=compartment_id,
        serving_mode=OnDemandServingMode(
            model_id="cohere.command-r-plus"
        ),
        chat_request=CohereChatRequest(
            message="107-0061の緯度と経度を教えてください",
            max_tokens=200,
        )
    )
)

print(f"response text: {response.data.chat_response.text}")
```

ちなみに、上記の国土地理院のAPIを使って〒107-0061（東京都港区北青山）の緯度と経度を調べると
[139.720947, 35.675377]
となります。
 
それぞれの回答
----------------------

- Command R
```
経度0061度、緯度107度です。
```
- Command R +
```
107-0061 は日本の住所で、緯度と経度はおよそ 35.6886°N, 139.7503°E です。
```

Command Rは見るからにハルシネーションを起こしています。(笑)
Command R +は誤差が0.01度以内と少なく、単体でも比較的精度が高いことが分かります。

### 4. API を使う関数の登録
下記2つの関数を登録します。
- query_address_from_postal_code
- get_adress_geometory

```Python
import requests
import urllib

def query_address_from_postal_code(postal_code: str) -> dict:
    if re.match("^[0-9]{3}-[0-9]{4}$", postal_code):
        postal_code = postal_code.replace("-", "")
    headers = {"content-type": "application/json"}
    response = requests.get(f"https://jp-postal-code-api.ttskch.com/api/v1/{postal_code}.json", headers=headers)
    data_ja = response.json()["addresses"][0]["ja"]
    if response.status_code == 200:
        return {
            "address": f"{data_ja['prefecture']}{data_ja['address1']}{data_ja['address2']}"
        }
    else:
        return {
            "address": "与えられた郵便番号に合致する住所は見つかりませんでした"
        }

def get_address_geometry(address: str):
        base_url = "https://msearch.gsi.go.jp/address-search/AddressSearch?q="
        encoded_address = urllib.parse.quote(address)
        url = base_url + encoded_address
        response = requests.get(url)
        
        if response.status_code == 200:
            data = response.json()
            if data and len(data) > 0:
                coordinates = data[0]["geometry"]["coordinates"]
                return coordinates  # [経度, 緯度] のリストを返す
            else:
                print("与えられた住所から住所を算出できませんでした")
        else:
            print(f"リクエストに失敗しました。status code: {response.status_code}")
    
```
### 5. Toolsを使って登録
CohereのToolsを使い、先ほど作成した関数をツールとして登録します。
こちらが必須パラメータとなっています。
- name
- description
- parameter_definitions

また、functions_mapでToolsの関数名と4セクションでで作成した関数のマッピングを行います。

```Python
tools = [
    CohereTool(
        name = "query_address_from_postal_code",
        description = "郵便番号を元に住所を検索します",
        parameter_definitions = {
            "postal_code": CohereParameterDefinition(
                description = "この郵便番号を元に住所を検索します。",
                type = "str",
                is_required = True
            )
        }
    ),
    CohereTool(
        name = "get_adress_geometory",
        description = "住所から緯度を検索します",
        parameter_definitions = {
            "address": CohereParameterDefinition(
                description = "この住所をもとに緯度を検索します。",
                type = "str",
                is_required = True
            )
        }
    )
]

functions_map = {
    "query_address_from_postal_code": query_address_from_postal_code,
    "get_adress_geometory":get_adress_geometory
}
```
### 6. 回答の確認
セクション3で書いたコードを書き換えます。

- preamble_override：チャットに事前情報としてタスクとコンテキスト、回答形式を指定します
- is_force_single_step：
有効化すると元のメッセージに直接応答する前に、1 つのステップで (潜在的に複数の) ツール呼び出しを発行
```Python
preamble_override = """
## タスクとコンテキスト
人々の質問やその他のリクエストにインタラクティブに答える手助けをする。2つの質問に答えてください。あらゆる種類のトピックについて、非常に幅広い質問を受けます。幅広い検索エンジンや類似のツールが用意されており、それらを使って答えを調べます。あなたは、ユーザーのニーズにできる限り応えることに集中すべきです。
## スタイルガイド
ユーザーから別の回答スタイルを要求されない限り、適切な文法とスペルを使い、完全な文章で回答してください。
"""

response = generative_ai_inference_client.chat(
    chat_details=ChatDetails(
        compartment_id=compartment_id,
        serving_mode=OnDemandServingMode(
            model_id="cohere.command-r-plus"
        ),
        chat_request=CohereChatRequest(
            message="107-0061の緯度と経度を教えてください",
            max_tokens=400,
            tools=tools,
            preamble_override=preamble_override,
            is_force_single_step=True
        )
    )
)


print(response.data)
```
### response.dataの出力の確認
"tool_calls"にてquery_address_from_postal_codeとget_adress_geometory関数と紐づくパラメータが確認できます。

```Python

{
  "chat_response": {
    "api_format": "COHERE",
    "chat_history": [],
    "citations": null,
    "documents": null,
    "error_message": null,
    "finish_reason": "COMPLETE",
    "is_search_required": null,
    "prompt": null,
    "search_queries": null,
    "text": "",
    "tool_calls": [
      {
        "name": "query_address_from_postal_code",
        "parameters": {
          "postal_code": "107-0061"
        }
      },
      {
        "name": "get_adress_geometory",
        "parameters": {
          "address": "\u6771\u4eac\u90fd\u6e2f\u533a\u5317\u9752\u5c711\u4e01\u76ee"
        }
      }
...
  },
  "model_id": "cohere.command-r-plus",
  "model_version": "1.2"
}
```
tool_calls に含まれるツール名とパラメータを使って、ツールを実行します。
```Python
tool_results = []

for tool_call in response.data.chat_response.tool_calls:
    output = functions_map[tool_call.name](**tool_call.parameters)
    outputs = [output]
    tool_results.append(
        CohereToolResult(
            call=CohereToolCall(
                name=tool_call.name,
                parameters=tool_call.parameters
            ),
            outputs=outputs
        )
    )

print(tool_results)
```
### 7.最終回答の確認
response.data.chat_response.textresponse.data.chat_response.textの回答を確認してみます。

```Python
response = generative_ai_inference_client.chat(
    chat_details=ChatDetails(
        compartment_id=compartment_id,
        serving_mode=OnDemandServingMode(
            model_id="cohere.command-r-plus"
        ),
        chat_request=CohereChatRequest(
            message="107-0061の緯度と経度を教えてください",
            chat_history=response.data.chat_response.chat_history,
            max_tokens=200,
            is_force_single_step=True,
            tools=tools,
            tool_results=tool_results,
            preamble_override=preamble_override
        )
    )
)
print(response.data.chat_response.text)
```
 
それぞれのモデルの最終回答
----------------------
求める回答：東経 139.720947 北緯 35.675377
- Command R
```
東京都港区北青山の郵便番号 107-0061 の緯度経度は、北緯35.675、東経139.720です。
```
- Command R +
```
郵便番号 107-0061 は、東京都港区北青山のエリアに対応しているようです。緯度と経度は、北緯35.67538、東経139.72095です。
```
Function Callingの結果を見ると
特にCommand Rの回答がかなりよくなることが確認できました！！
Command R +の回答も日本の住所→東京都北青山と具体的になりました。
## 裏話
実は、```message="107-0061の住所を求め、その住所の緯度と経度を教えてください"```
というプロンプトを最初は書いていましたが、郵便番号APIだけが使われて国土地理院APIは
使用されずハルシネーションを起こしていました・・・涙
適切な入力プロンプトにすることがFunction Callingにおいて大事なんだなと・・・
~~(絶対最初のプロンプトの方がいいと思うんだけどな^^)~~
もし、うまくFunction Callingできない場合は入力プロンプトを変えて試してみてください。

## 超おまけ）Command R＋の関西弁が好きすぎる
preamble_overrideのタスクを書き直します。
```Python
preamble_override = """
## タスクとコンテキスト
あなたは陽気なアシスタントです。関西弁で話してください。
"""

response = generative_ai_inference_client.chat(
    chat_details=ChatDetails(
        compartment_id=compartment_id,
        serving_mode=OnDemandServingMode(
            model_id="cohere.command-r-plus"
        ),
        chat_request=CohereChatRequest(
            message="107-0061の緯度と経度を教えてください",
            max_tokens=400,
            tools=tools
            preamble_override=preamble_override,
            is_force_single_step=True
        )
    )
)

print(response.data.chat_response.text)
```
### 結果
```Python
ええやんええやん！調べた結果、107-0061は東京都港区北青山一丁目に対応しとって、緯度経度はおおよそ次の値やな。

緯度: 35.669076
経度: 139.717096

青山通りと外苑西通りの交差点の近くや。近くに青山霊園もあるで。
```
私ほんとにプログラミング苦手なんですが、初めて楽しいと思えました。^^
元気出るので疲れたときのCommand R +、関西弁お勧めです。