OCI Generative AI Service の Cohere Tool Use を使って OCI Monitoring のメトリクスを自然言語で問い合わせてみる

## はじめに

以前、[OpenAI の関数呼び出し (Function Calling) 機能を使って Oracle Cloud Infrastructure のメトリクスを自然言語で問い合わせてみる](https://qiita.com/tkote/items/0dcf64cfba7e310041b5) という記事を投稿しましたが、同等の機能が OCI Generative AI Service でも実現できるようになったので、OCI 版に焼き直してみたいと思います。

やりたいことは

+ CPUの使用率が80%以上のコンピュートを使用率と併せて教えて

のようなリアルタイムな問い合わせをクエリ式を考えたりコンソールいじったりせず自然言語の問い合わせで実現することです。

実装は、 Cohere Command R/R+ の Tool Use という機能を使いますが、この解説は、[こちら](https://qiita.com/shukawam/items/14ee6efc69f75ace0a0e) や [こちら](https://qiita.com/msasakaw/items/21ce7f48f60ae97ed778) を参照していただければと思います。
Cohere のドキュメンテーションは [こちら](https://docs.cohere.com/docs/tools)。

Tool Use を使うと LLM のモデルが知らない情報も、自分が定義した関数を使って回答に取り込むことができます。

大まかな流れは以下の通り
1. あらかじめ関数（複数可）を定義しておく
1. 関数の仕様情報を付加して Chat に問い合わせる
1. Chat が回答を作成するのに適切な関数を見つけた場合、パラメータと共に関数を呼び出す指示をレスポンスとして返す
1. 指示に従って関数を呼び出し、その結果と Chat ヒストリーを付加して再度 Chat に問い合わせる
1. 呼び出すべき関数の指示がなくなるまで Chat の呼び出しを繰り返す
1. Chat から最終的な回答を取得する

## 事前準備： OCI Monitoring のメトリクスを取得する関数を作る

このあたりは、前回投稿の記事と同じです。違うのは、パラメータを適切な名前に変えたのと、Cohere の場合は返り値が辞書型だということ。

```python
import oci, os, json
from datetime import datetime, timedelta, timezone

def get_metrics(metric_type, inequality_sign = None, boundary_value = None):
    """ 
    コンピュートのメトリクス （cpu or memory） を取得する関数
    直近1分間の utilization 平均値 1レコードを取得する
    """

    # 検索カテゴリーは "cpu" か "memory" のいずれか、1分間の平均使用率
    metric_types = {
        "cpu" : "CPUUtilization[1m].mean()",
        "memory" : "MemoryUtilization[1m].mean()"
    }

    query = metric_types[metric_type.lower()]

    signer = oci.auth.signers.InstancePrincipalsSecurityTokenSigner()
    config = {'region': signer.region, 'tenancy': signer.tenancy_id}
    metric_client = oci.monitoring.MonitoringClient(config, signer=signer)

    metric_detail = oci.monitoring.models.SummarizeMetricsDataDetails()
    metric_detail.query = query

    # とりあえず最新のデータだけを取得する仕様にする
    now = datetime.now(tz=timezone.utc)
    metric_detail.start_time = (now - timedelta(minutes=1)).isoformat()
    metric_detail.end_time = now.isoformat()

    metric_detail.resolution = "1m"
    metric_detail.namespace = "oci_computeagent"
    compartment_id = os.getenv("COMPARTMENT_ID")

    metrics = metric_client.summarize_metrics_data(compartment_id, metric_detail).data

    results = []
    for m in metrics:
        timestamp = m.aggregated_datapoints[0].timestamp
        value = m.aggregated_datapoints[0].value
        resourceDisplayName = m.dimensions["resourceDisplayName"]

        if not inequality_sign or \
          (inequality_sign.lower() == "gt" and value > boundary_value) or \
          (inequality_sign.lower() == "ge" and value >= boundary_value) or \
          (inequality_sign.lower() == "le" and value <= boundary_value) or \
          (inequality_sign.lower() == "lt" and value < boundary_value) :
            results.append({
                "displayName" : resourceDisplayName,
                "timestamp" : timestamp.isoformat(),
                "value" : value
            })

    print(f">>> results\n{json.dumps(results, indent=2)}\n<<<")
    return {"query" : query, "metrics" : results}
```
パラメータの指定が若干トリッキーなので、LLMが関数の仕様をちゃんと理解してくれるかどうか...

## Tool Use を使った問い合わせを作る

LLM に問い合わせる本体の関数を作ります。

```python
from oci.generative_ai_inference import GenerativeAiInferenceClient
from oci.generative_ai_inference.models import (
  ChatDetails, CohereChatRequest, OnDemandServingMode,
  ChatResult, CohereChatResponse, 
  CohereTool, CohereParameterDefinition, CohereToolResult, CohereToolCall
)

def query_metrics(question):
    """ Tool Use を使った Cohere 問い合わせ """

    print(question)
    compartment_id = os.environ["COMPARTMENT_ID"]
    config = oci.config.from_file()
    config["region"] = "us-chicago-1"
    model_id = "cohere.command-r-plus" # or cohere.command-r-16k

    client = GenerativeAiInferenceClient(config=config)

    # tool の定義 - 関数のスペックを記述する
    tools = [
        CohereTool(
            name = "get_metrics",
            description = 
                "Get resource metrics retrieved by agents in compute instances. " +
                "It returns 'displayName', 'timestamp' and 'value' for each resource. " + 
                "'displayName' is a name of compute instances. It must be displayed as is, and it must not be translated to other languages. " +
                "'value' is always expressed as percentage, hence if 'value' is 0.2, it is not '20%' but '0.2%'. " +
                "The type of metrics is either cpu or memory. 'displayName' cannot be translated.",
            parameter_definitions = {
                "metric_type": CohereParameterDefinition(
                    description = "The type of the resource metrics. it must be either 'cpu' or 'memory'.",
                    type = "str",
                    is_required = True
                ),
                "inequality_sign": CohereParameterDefinition(
                    description = "The condition to filter values, it can be ommited when you want to get metrics of all resources. " +
                    "The value must be one of 'gt', 'ge', 'le', 'lt'. " + 
                    "'以上' は 'ge' で表現され、'以下' は 'le' で表現されます。",
                    type = "str",
                    is_required = False
                ),
                "boundary_value": CohereParameterDefinition(
                    description = "The value to filter, expressed as percentage.",
                    type = "float",
                    is_required = False
                )
            }
        )
    ]

    # 関数名と実体とのマップを作っておく（後で実際に関数を呼び出す際に使う）
    functions_map = {
        "get_metrics" : get_metrics
    }

    # 最初の Chat 呼び出し
    preamble = "あなたは有能な日本人のデータセンタ管理者です。全力で丁寧に回答してください。"
    response = client.chat(
        ChatDetails(
            compartment_id = compartment_id,
            chat_request = CohereChatRequest(
                preamble_override = preamble,
                message = question,
                max_tokens = 4000,
                tools = tools,
                is_force_single_step = False
            ),
            serving_mode = OnDemandServingMode(model_id = model_id)
        )
    ).data # type: ChatResult

    cohere_response = response.chat_response # type: CohereChatResponse

    # tool_calls が null になるまでループ、関数を呼び出した結果を付加して Chat を再度呼び出す
    while cohere_response.tool_calls:
        tool_calls = cohere_response.tool_calls # type: list[CohereToolCall]

        # 関数を呼び出して、結果を集める
        tool_results = []
        for tool_call in tool_calls:
            print(f">>> tool_call\n{tool_calls}\n<<<")
            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
                )
            )

        # n回目の Chat 呼び出し
        response = client.chat(
            chat_details = ChatDetails(
                compartment_id = compartment_id,
                serving_mode = OnDemandServingMode(
                    model_id = model_id
                ),
                chat_request = CohereChatRequest(
                    preamble_override = preamble,
                    message = "",
                    max_tokens = 4000,
                    chat_history = cohere_response.chat_history,
                    tools = tools,
                    tool_results = tool_results
                )
            )
        ).data # type: ChatResult
        cohere_response = response.chat_response # type: CohereChatResponse
        
    return cohere_response.text
```

`cohere_response.tool_calls` が null でない、すなわちレスポンスで関数呼び出しの指示がなされたら、指定されたパラメータを使ってその関数を呼び出し、その結果と Chat ヒストリーを追加しながら再びリクエストを行い、関数呼び出しの指示がなくなるまでループして最終的な回答を導き出す、ということをやっています。

## 実行してみる

以下のような問い合わせを試してみます。

```python
print(query_metrics("CPUの使用率をMarkdownの表にして"))
print("\n-----")
print(query_metrics("CPUの使用率が5%以下のものを全て、使用率と併せて教えてください"))
print("\n-----")
print(query_metrics("メモリー使用率を小数点第一位まで四捨五入して教えてください"))
print("\n-----")
print(query_metrics("メモリー使用率が20%より大きいものは？使用率は？"))
```

実行結果

```txt
CPUの使用率をMarkdownの表にして
>>> tool_call
[{
  "name": "get_metrics",
  "parameters": {
    "metric_type": "cpu"
  }
}]
<<<
>>> results
[
  {
    "displayName": "coala",
    "timestamp": "2024-06-23T09:31:00+00:00",
    "value": 5.112279620935162
  },
  {
    "displayName": "panda",
    "timestamp": "2024-06-23T09:31:00+00:00",
    "value": 3.7214550897782375
  }
]
<<<
```
| 名前 | CPU使用率 |
|---|---|
| coala | 5.11% |
| panda | 3.72% |
```

-----
CPUの使用率が5%以下のものを全て、使用率と併せて教えてください
>>> tool_call
[{
  "name": "get_metrics",
  "parameters": {
    "boundary_value": 5,
    "inequality_sign": "le",
    "metric_type": "cpu"
  }
}]
<<<
>>> results
[
  {
    "displayName": "panda",
    "timestamp": "2024-06-23T09:31:00+00:00",
    "value": 3.7214550897782375
  }
]
<<<
CPU使用率が5%以下のものは、panda (3.72%) です。

-----
メモリー使用率を小数点第一位まで四捨五入して教えてください
>>> tool_call
[{
  "name": "get_metrics",
  "parameters": {
    "metric_type": "memory"
  }
}]
<<<
>>> results
[
  {
    "displayName": "coala",
    "timestamp": "2024-06-23T09:31:00+00:00",
    "value": 23.92735950578827
  },
  {
    "displayName": "panda",
    "timestamp": "2024-06-23T09:31:00+00:00",
    "value": 9.693749907154716
  }
]
<<<
coalaは24.0%、pandaは9.7%です。

-----
メモリー使用率が20%より大きいものは？使用率は？
>>> tool_call
[{
  "name": "get_metrics",
  "parameters": {
    "boundary_value": 20,
    "inequality_sign": "gt",
    "metric_type": "memory"
  }
}]
<<<
>>> results
[
  {
    "displayName": "coala",
    "timestamp": "2024-06-23T09:31:00+00:00",
    "value": 23.92735950578827
  }
]
<<<
coalaのメモリー使用率は23.93%です。
```

いい感じに関数のパラメータが指定されているのがわかると思います。

## まとめ

OCI上で稼働しているコンピュートのメトリクスを自然言語で問い合わせてみました。リアルタイムな情報を検索する場合は、今回使った Tool Use、一般的には Agent と呼ばれるような仕組みを使って実現できますね。

今回色々やってみて、LLMが正しく振る舞うように指示を与えるのはやっぱり結構難しいなと感じました。「以上」や「以下」の表現が正しいパラメータに変換できない部分は、　`'以上' は 'ge' で表現され、'以下' は 'le' で表現されます。` という description の記述で正しく動作するようになりましたが、パーセント表記の `0.2` を勝手に `20%` と誤って解釈したり、リソース名の `panda` を気まぐれに `パンダ` と表示してみたりと、なかなか手強いです。

Command R+ 実は OCI Monitoring の MQL (Monitoring Query Language) を生成する能力がそこそこありそうなので、Tool Use から呼び出す関数はもっと汎用的に作ることができるかも？と考えています。これはもう少し研究してみよう。

Tool Use は Cohere の機能ですが、今回紹介したコードは全て OCI Generative AI Service の SDK で実装していて、呼び出しているのは OCI Generative AI Service の endpoint です。