【Python】OCI Generative AI Agent の SDK を解剖する

# はじめに

OCI Generative AI Agent は ノーコード でも利用できますが、 Python SDK を使えば CLI や別アプリとの連携が格段に楽になります。実際に実装してみたというレポートは、様々な人が書いていることが確認できます。

しかし、意外なことに公式ドキュメントベースの議論が多くありません。そこで、主要クラスとメソッドを読みながら、それをもとに体系的に実装してみたので記事にまとめてみました。

なお、OCI Python SDK 公式ドキュメントについては OCI Generative AI Agent に限定しています。

# OCI AI Agent
OCI Generative AI Agent とは、 AI Agent をフルマネージドで提供する web サービスです。こちらには、現在 RAG / SQL / Functions の３モードがあります。

本記事では RAG 機能に絞った記事となります。

# 関連記事

https://qiita.com/msasakaw/items/219c58ecdf98b4bfb743

こちらの記事は OCI Generative AI Agent の実装方法を紹介しています。 OCI の GUI での作成方法をわかりやすく記述しています。今回の実装もこの記事を参考にして OCI Generative AI Agent を作りました。

注意すべき点は、記事に書いてある通りにシカゴリージョンを使う必要は無くなっていることです。正確にいうと利用可能なリージョンは、[こちら](https://docs.oracle.com/en-us/iaas/api/#/en/generative-ai-agents-client/20240531/)で提示されている5つのリージョンとなります。

https://qiita.com/karashi_moyashi/items/78a1dcf71a552b746ab3

こちらの記事は OCI Generative AI Agent の精度を試してみた記事です。新しい機能である PDF のグラフ読み取りについて検証しています。

検証結果としては、不安定ながらも期待大であるとの評価でした。

https://qiita.com/shukawam/items/c9eb9cd37950150d4d70

こちらの記事は、 OCI Generative AI Agent のコードでの実行方法が記述されています。 OCI Python SDK を使ってアクセスしています。議論の進み方が Jupyter Notebook のような形に近いため、再現実験が簡単だと思います。

参考になる場面は

- CUI 上で手っ取り早く結果が欲しい場合
- API レスポンスの形式を確認したい場合

などの場面です。

https://qiita.com/karashi_moyashi/items/43d9f1c12eabf26c3e39

この記事は、関連記事2を実装した上で、 Gradio という Web UI でラッピングするという記事です。もしデモ用 Web アプリを作りたい場合、この記事1本読むだけで満足できるはずです。

この記事の特徴は、UI コードとのインテグレーションを紹介をしている点と、仮想環境の準備からウォークスルーしている点です。

# SDK 大解剖！

主要クラスは `GenerativeAiAgentClient` と `GenerativeAiAgentRuntimeClient` の２つです。

- `GenerativeAiAgentClient` ：このクラスは管理を主目的とするものです。 OCI Generative AI Agent のインスタンスやエンドポイントの検索・作成・削除などが可能です。
- `GenerativeAiAgentRuntimeClient` ：このクラスは、実行時に利用するものです。 OCI Generative AI Agent の呼び出しやチャットなどが可能です。

## oci.generative_ai_agent.GenerativeAiAgentClient
[ドキュメンテーション](https://docs.oracle.com/en-us/iaas/tools/python/2.150.2/api/generative_ai_agent/client/oci.generative_ai_agent.GenerativeAiAgentClient.html#oci.generative_ai_agent.GenerativeAiAgentClient)


### oci.generative_ai_agent.GenerativeAiAgentClient.\_\_init\_\_

**引数**

GenerativeAiAgentClientのインスタンス化には `config` 引数が必要です。
- `config` ：この引数は、 `oci.config.from_file` メソッドの戻り値を入れる引数です。
  - `oci.config.from_file` は引数なしで実行すると、 `~/.oci/config` を読み込みます。もしクレデンシャルファイルを任意のパスにしたい場合、 `oci.config.from_file.file_location` 引数に入れると動きます。



<details><summary>使用例</summary>


```python
import oci
from oci.generative_ai_agent import GenerativeAiAgentClient

config = oci.config.from_file()
# または
config = oci.config.from_file(file_location = "~/config_directory/config")

# インスタンス化
client_agent = GenerativeAiAgentClient(config)
```

</details>

### oci.generative_ai_agent.GenerativeAiAgentClient.list_agents

list_agents メソッドでは、作った OCI Generative AI Agent のリスト取得ができます。

**引数**

- `compartment_id` ：この引数は、コンパートメント id を期待します。入力したコンパートメント id で作られたエージェントの検索ができます。
- `display_name` ：この引数は、エージェントの名前を入れることができます。 `compartment_id` 引数の使い方同様に、入力した名前でエージェントを検索することが可能です。

**戻り値**

戻り値は [oci.generative_ai_agent.models.AgentCollection 型](https://docs.oracle.com/en-us/iaas/tools/python/2.150.2/api/generative_ai_agent/models/oci.generative_ai_agent.models.AgentCollection.html#oci.generative_ai_agent.models.AgentCollection
)です。 `AgentCollection` 型で重要となる情報は以下です。
- `oci.generative_ai_agent.models.AgentSummary.id` ：この型は `AgentCollection.items` アトリビュートにリストで格納されている `AgentSummary` のアトリビュートです。エージェントを一意に定める id となります。次節 `oci.generative_ai_agent.GenerativeAiAgentClient.list_agent_endpoints` メソッドで必要になる情報です。

<details><summary>使用例</summary>


```python
from typing import List
from operator import attrgetter

from oci.generative_ai_agent.models import AgentCollection
from oci.generative_ai_agent.models import AgentSummary

# oci.generative_ai_agent.GenerativeAiAgentClient をインスタンス化するコードは省略

collection_agent: AgentCollection = client_agent.list_agents(
    compartment_id = "ocid1.test.oc1..<unique_ID>EXAMPLE-compartmentId-Value"
    display_name = "<OCI Generative AI Agent Name>"
).data

summaries_agent: List[AgentSummary] = collection_agent.items

# 一意に定まる場合、 ids_agent は長さ1のリスト。
ids_agent = list(map(attrgetter("id"), summaries_agent)) 
```

</details>

### oci.generative_ai_agent.GenerativeAiAgentClient.list_agent_endpoints

list_agent_endpoints メソッドでは、作った OCI Generative AI Agent のエンドポイントのリストの取得ができます。

**引数**

- `compartment_id` ：この引数は、コンパートメント id を期待します。入力したコンパートメント id で作られたエージェントのエンドポイントの検索ができます。
- `agent_id` ：この引数は、 `list_agent` メソッドで取得できるエージェント id を入れることができます。 `compartment_id` 引数の使い方同様に、入力した id でエージェントエンドポイントを検索することが可能です。

**戻り値**

戻り値は [oci.generative_ai_agent.models.AgentEndpointCollection 型](https://docs.oracle.com/en-us/iaas/tools/python/2.150.2/api/generative_ai_agent/models/oci.generative_ai_agent.models.AgentEndpointCollection.html#oci.generative_ai_agent.models.AgentEndpointCollection)です。以下は主要な情報です。
- `oci.generative_ai_agent.models.AgentEndpointSummary.id` ：この型は `AgentEndpointCollection.items` アトリビュートにリストで格納されている `AgentEndpointSummary` のアトリビュートです。エージェントエンドポイントを一意に定める id となります。次章 `GenerativeAiAgentRuntimeClient` クラスにおけるメソッド全てで必要になる情報です。


<details><summary>使用例</summary>

```python
from typing import List
from operator import attrgetter

from oci.generative_ai_agent.models import AgentEndpointCollection
from oci.generative_ai_agent.models import AgentEndpointSummary

# oci.generative_ai_agent.GenerativeAiAgentClient をインスタンス化するコードは省略

collection_endpoint_agent: AgentEndpointCollection = client_agent.list_agent_endpoints(
    compartment_id = "ocid1.test.oc1..<unique_ID>EXAMPLE-compartmentId-Value"
    agent_id = "ocid1.test.oc1..<unique_ID>EXAMPLE-agentId-Value"
).data

summaries_endpoint_agent: List[AgentSummary] = collection_endpoint_agent.items

# 1エージェントに１つしかエンドポイントを作っていない場合、 ids_endpoint_agent は長さ1のリスト
ids_endpoint_agent = list(map(attrgetter("id"), summaries_endpoint_agent)) 
```

</details>

## oci.generative_ai_agent_runtime.GenerativeAiAgentRuntimeClient
[ドキュメンテーション](https://docs.oracle.com/en-us/iaas/tools/python/2.150.2/api/generative_ai_agent_runtime/client/oci.generative_ai_agent_runtime.GenerativeAiAgentRuntimeClient.html#oci.generative_ai_agent_runtime.GenerativeAiAgentRuntimeClient)

### oci.generative_ai_agent_runtime.GenerativeAiAgentRuntimeClient.\_\_init\_\_

**引数**

インスタンス化するために主要な変数は2つあります。
- `config` ：この引数は、 `GenerativeAiAgentClient` クラスと同様に `oci.config.from_file` メソッドの戻り値を入れる引数です。
- `service_endpoint` ：この引数は、 OCI Generative AI Agent を作った時のサービスのエンドポイントを期待します。[こちら](https://docs.oracle.com/en-us/iaas/api/#/en/generative-ai-agents-client/20240531/)のエンドポイントから選びます。何も設定しない場合、 `config` パラメタのリージョンを参考にして自動的に設定してくれます。

:::note alert
`service_endpoint` の明示的な設定は非推奨

他の記事では明示的に設定しているため紹介しましたが、実は `service_endpoint` の明示的な設定はドキュメンテーション的には非推奨とされています。触らないようにしましょう。
:::


<details><summary>使用例</summary>


```python
import oci
from oci.generative_ai_agent_runtime import GenerativeAiAgentRuntimeClient

config = oci.config.from_file()

# または
config = oci.config.from_file(file_location = "~/config_directory/config")

client_runtime_agent = GenerativeAiAgentRuntimeClient(config)
```

</details>

### Session ID 管理

OCI Generative AI Agent では、**セッション**という概念を用いてチャットのスレッドを保持します。チャットを API リクエストで実行するため、状態の保持をセッションで実現しています。

具体的には、セッション id と呼ばれる文字列を発行し、チャットリクエストに合わせて送信します。 OCI の API サーバでは、セッション id は会話履歴と紐つけられています。これにより、 OCI Generative AI Agent に会話履歴を考慮した返答をさせることが可能となります。

`create_session` 、 `delete_session` メソッドでセッション id を発行し、失効させることができます。

:::note info
`update_session` と`get_session` の省略

`update_session` と `get_session` は、機能が少なく、必要な場面が少ないため、省略します。
:::

**引数**

`create_session` メソッドでは `agent_endpoint_id` と `create_session_details` が必要です。

対して、 `delete_session` メソッドでは `agent_endpoint_id` と `session_id` が必要になります。

- `agent_endpoint_id` ：この引数に、 `oci.generative_ai_agent.GenerativeAiAgentClient.list_agent_endpoints` メソッドの戻り値で取得できる、 `oci.generative_ai_agent.models.AgentEndpointSummary.id` を入れる必要があります。
- `create_session_details` ：この引数は `oci.generative_ai_agent_runtime.models.CreateSessionDetails` 型です。 `CreateSessionDetails` のインスタンス化において、特に必要となる引数はありません。
- `session_id` ：セッション id です。

**戻り値**

`create_session` メソッドの戻り値の型は `oci.generative_ai_agent_runtime.models.Session` で、 `delese_session` の戻り値は `None` です。

`oci.generative_ai_agent_runtime.models.Session` で主要なアトリビュートは２つあります。
- `id` ：このアトリビュートはセッション id を表します。
- `welcome_message` ：このアトリビュートは OCI Generative AI Agent を作った時に設定したウェルカムメッセージです。

:::note warn
セッション id の有効期限

セッション id は1時間放置した場合、失効するので注意が必要です。

設定により7日放置で失効に伸ばせます。

https://docs.oracle.com/en-us/iaas/Content/generative-ai-agents/limits.htm
:::


<details><summary>使用例</summary>

```python
from oci.generative_ai_agent_runtime.models import CreateSessionDetails

# oci.generative_ai_agent_runtime.GenerativeAiAgentRuntimeClient をインスタンス化するコードは省略
# ids_endpoint_agent を取得するコードは省略

session_runtime_agent = client_runtime_agent.create_session(
    agent_endpoint_id = ids_endpoint_agent[0]
    create_session_details = CreateSessionDetails()
).data

id_session_runtime_agent = session_runtime_agent.id
message_welcome = session_runtime_agent.welcome_message

client_runtime_agent.delete_session(
    agent_endpoint_id = ids_endpoint_agent[0]
    session_id = id_session_runtime_agent
)
```

</details>

### oci.generative_ai_agent_runtime.GenerativeAiAgentRuntimeClient.chat
このメソッドは生成を実行します。

主要な引数は３つ（２つ）です。

- `agent_endpoint_id` ：この引数は、セッション管理の時と同様に、 `oci.generative_ai_agent.models.AgentEndpointSummary.id` を入れます。
- `chat_details` ：この引数は、 `oci.generative_ai_agent_runtime.models.ChatDetails` 型で指定します。後述します。
- （ `allow_control_chars` ）：この引数は、 `True` にすると改行　`\n`　やタブ　`\t`　などが出力されるようになる・・・と思いがちですがそのようなわけではありません。改行などの表現はこの引数がどうであれ出力されます。 SDK の内部で通信している API からのバイト型レスポンスにおいて、改行を表現したい場合 `0x0A` ではなく `0x5C0x6E` として受信するためです。 API レスポンスでの制御文字を許すか許さないかという、もっと深いところでのフラグです。 JSON 規約的に制御文字は許さない方針なので触らないほうが良いかと思います。

出力形式は `oci.generative_ai_agent_runtime.models.ChatResult` 型です。主要なアトリビュートは以下です。

- `message` ：このアトリビュートは `oci.generative_ai_agent_runtime.models.Message` 型のデータです。 AI の返答は `message.text` に文字列として格納されています。参考文献は、 `message.citations` に、 `oci.generative_ai_agent_runtime.models.Citation` のリストとして格納されています。

<details><summary>使用例</summary>

```python
from oci.generative_ai_agent_runtime.models import ChatDetails

# oci.generative_ai_agent_runtime.GenerativeAiAgentRuntimeClient をインスタンス化するコードは省略
# ids_endpoint_agent を取得するコードは省略
# id_session_runtime_agent を取得するコードは省略

# ChatDetails の内容は次節参照
response_chat = client_runtime_agent.chat(
    agent_endpoint_id = ids_endpoint_agent[0]
    chat_details = ChatDetails(
        session_id = id_session_runtime_agent
        user_message = "こんにちは"
    )
)

message_response_chat = response_chat.message.text
citations_response_chat = response_chat.message.citations
```

</details>

#### oci.generative_ai_agent_runtime.models.ChatDetails
`ChatDetails` 型のインスタンス化に主要な引数は３つあります。

**引数**
- `session_id` ：セッション id です。
- [should_stream](https://docs.oracle.com/en-us/iaas/tools/python/2.150.2/api/generative_ai_inference/models/oci.generative_ai_inference.models.CohereChatRequest.html#oci.generative_ai_inference.models.CohereChatRequest.is_stream) ：この引数は SSE での出力をするか否かを決めるブーリアンです。 `True` にすると `CohereChatRequest.is_stream` よろしくぽこじゃが出力される・・・と思いがちですが、そのようなわけでもありません。 OCI Generative AI Agent の**思考過程と検索処理などの途中経過**をストリームとして出力するか否かを指定するものです。また、この引数を変えた際の出力形式には注意が必要です。 `False` にするとチャットの戻り値は `ChatResult` 型となりますが、 `True` だと `oci._vendor.sseclient.SSEClient` 型となります。 SSE の受信時の具体的型は、 `{'traces': List[Trace]}` が複数回出力されたあと、`{'message': ChatResult}` で終わるような型です。
- `user_message` ：この引数は AI に送るメッセージです。このメッセージを元に AI が返答を生成します。

# 実装してみる

今回実装するものは、上記主要クラス２つを扱ったチャットボットです。議論の発散を防ぐため、今回は CUI のみでの実装をします。

Python SDK による OCI Generative AI Agent の実装そのものは様々な記事で紹介されていますが、今回の実装は、1）オブジェクトとして扱う、2） `GenerativeAiAgentClient` も考慮に入れて実装する、という２点で参考になると思います。

## 0. Agent を作る

まず初めに Agent を OCI で作らなければ呼び出すものも呼び出せません。

具体的な手順はこちらの記事を参照してください。

https://qiita.com/msasakaw/items/219c58ecdf98b4bfb743

今回の例で取り込むデータは、Allganize 社が公開している RAG 評価用データセット　[Allganize RAG Leaderboard](https://huggingface.co/datasets/allganize/RAG-Evaluation-Dataset-JA)　を利用します。インターネット上の PDF を情報源として、テスト用の質問と模範回答が用意されているため、 RAG の精度を同時に測定できる仕組みになっています。

例えば、公益財団法人生命保険文化センターが公開している「[2021（令和3）年度 生命保険に関する全国実態調査](https://www.jili.or.jp/research/report/8361.html)」の PDF 「調査要領・報告書を読むにあたって・調査結果の概要 (PDF:437KB)」について、次のような質問があります。

> 世帯主が万が一の事態により3ヶ月間入院した場合、必要な資金はいくら必要と考えられていますか？

該当箇所は14ページです。下図は抜粋したキャプチャです。

![PDF抜き出し](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4071290/838446a3-4ebf-4da2-b1b9-a37b3ca2a2c9.png)

> ●世帯主が２～３カ月入院した場合に、必要と考える資金額は、月額24.2万円となっている。
世帯主が２～３カ月入院した場合に、健康保険診療の範囲外の費用として必要と考える資金額を尋ねたところ、平均月額は24.2万円（前回25.7万円）となっている。

この問いに対する模範回答は次の通りです。

> 世帯主が2～3ヶ月間入院する場合、健康保険診療の範囲外の費用として必要と考える資金額は月間24.2万円と考えられています。したがって、3ヶ月入院した場合は72.6万円（24.2万円×3ヶ月）が求められます。

この模範回答が無事生成できるのか、今回の実装の動作確認として試してみたいと思います。


## 1. main 関数
```python:app.py
if __name__ == "__main__":
    config_file_path = "~/config/config" # oci config ファイルへの絶対バス
    agent_name = "evaluation-rag"

    agent_client: AgentClient = AgentClient(path_config_file=config_file_path)
    agent_runtime: AgentRuntime = agent_client.new_runtime(agent_name)

    message = "世帯主が万が一の事態により3ヶ月間入院した場合、必要な資金はいくら必要と考えられていますか？"

    response: ChatResult = agent_runtime.chat(message)

    # 模範回答：「世帯主が2～3ヶ月間入院する場合、健康保険診療の範囲外の費用として必要と考える資金額は月間24.2万円と考えられています。したがって、3ヶ月入院した場合は72.6万円（24.2万円×3ヶ月）が求められます。」
    print(f"User Input: {message}\nChat Result: {response.message.content.text}")

    message = "２ヶ月では何円？"
    response_stream_iter: Iterator[Dict] = agent_runtime.chat_stream(message)

    # 模範回答：「世帯主が2～3ヶ月間入院する場合、健康保険診療の範囲外の費用として必要と考える資金額は月間24.2万円と考えられています。したがって、2ヶ月入院した場合は48.4万円（24.2万円×2ヶ月）が求められます。」
    for response_stream in response_stream_iter:
        if "message" in response_stream:
            print(
                f"User Input: {message}\nStream Chat result: {response_stream['message']['content']['text']}"
            )

    agent_runtime.end_session()


```
クラスを２つ作ってみました。
- `AgentClient` ：このクラスは `GenerativeAiAgentClient` のインスタンスを扱うクラスとなります。エージェントの名前からエンドポイントを検索・取得し、 `AgentRuntime` を払い出します。

- `AgentRuntime` ：このクラスは `GenerativeAiAgentRuntimeClient` のインスタンスを扱うクラスとなります。このクラスでチャットなどをしようかと考えています。


チャットでは、 `stream` が `True` の場合と `False` の場合両方について実装しようかと考えています。また、セッション id で本当に会話履歴を考慮して回答してくれるのかのテストのために「２ヶ月では何円？」という質問を新たに加えています。

## 2. AgentClient クラスの定義
### 骨組み
```python:app.py
class AgentClient:

    def __init__(self, config_file_path=None):
        pass

    def new_runtime(self, agent_name) -> AgentRuntime:
        pass
```

### 初期化関数
```python:app.py
from oci.generative_ai_agent import GenerativeAiAgentClient
from oci.generative_ai_agent_runtime import GenerativeAiAgentRuntimeClient

class AgentClient:

    def __init__(self, config_file_path=None):
        config = oci.config.from_file(file_location=config_file_path)
        self.compartment_id = "ocid1.test.oc1..<unique_ID>EXAMPLE-compartmentId-Value"
        self.oci_client_agent = GenerativeAiAgentClient(config)
        self.oci_runtime_agent = GenerativeAiAgentRuntimeClient(config)
```

`GenerativeAiAgentClient.list_agents` メソッドや `GenerativeAiAgentClient.list_agent_endpoints` メソッドにはコンパートメント id が必要なので、ここで変数にしておきましょう。

### new\_runtime
```python:app.py
class ClientAgent:

    def new_runtime(self, agent_name) -> AgentRuntime:
        agent_id: str = (
            self.oci_client_agent.list_agents(
                lifecycle_state="ACTIVE",
                compartment_id=self.compartment_id,
                display_name=agent_name,
            )
            .data.items[0]
            .id
        )
        agent_endpoint_id: str = (
            self.oci_client_agent.list_agent_endpoints(
                lifecycle_state="ACTIVE",
                compartment_id=self.compartment_id,
                agent_id=agent_id,
            )
            .data.items[0]
            .id
        )
        return AgentRuntime(self.oci_runtime_agent, agent_endpoint_id)
```

`AgentRuntime` という自作クラスのインスタンスを返す関数です。
このインスタンスの中で、 `GenerativeAiAgentRuntimeClient.create_session` や `GenerativeAiAgentRuntimeClient.chat` を実行します。

## 3. AgentRuntime クラスの定義
### 骨組み
```python:app.py
from oci.generative_ai_agent_runtime import GenerativeAiAgentRuntimeClient
from oci.generative_ai_agent_runtime.models import ChatResult

class AgentRuntime:

    def __init__(
        self,
        oci_agent_runtime: GenerativeAiAgentRuntimeClient,
        agent_endpoint_id: str,
    ):
        pass

    def enc_session(self) -> None:
        pass

    def chat(self, message: str) -> ChatResult:
        pass

    def chat_stream(
        self, message: list
    ) -> Iterator[Dict[str, Union[List[Trace], ChatResult]]]:
        pass
```
とりあえずの骨組み

### 初期化関数
```python:app.py
from oci.generative_ai_agent_runtime import GenerativeAiAgentRuntimeClient
from oci.generative_ai_agent_runtime.models import Session

class AgentRuntime:

    def __init__(
        self,
        oci_agent_runtime: GenerativeAiAgentRuntimeClient,
        agent_endpoint_id: str,
    ):
        self.oci_agent_runtime = oci_agent_runtime
        self.agent_endpoint_id = agent_endpoint_id
        session_response: Session = self._create_session()
        self.session_id = session_response.id
        self.chat_history = [session_response.welcome_message]

```

初期化時に `self._create_session()` でセッションを取得することにしましょう。
チャット履歴の保持はクライアント側でする必要があるため、リストとして持っておきます。

:::note info
チャット履歴は API で取得不可能

API では、セッション id を用いてチャット関数を実行すると、チャットの履歴を考慮した回答が返ってきます。

チャット履歴のデータを保持していると推察されますが、チャットの履歴を取得するための API は用意されていません。

つまりは、こちらのプログラム側でチャット履歴を保持する必要があります。

:::

### \_create\_session
```python:app.py
from oci.generative_ai_agent_runtime import GenerativeAiAgentRuntimeClient
from oci.generative_ai_agent_runtime.models import Session

class AgentRuntime:

    def _create_session(self) -> Session:
        session_response: Session = self.oci_runtime_agent.create_session(
            create_session_details=CreateSessionDetails(),
            agent_endpoint_id=self.agent_endpoint_id,
        ).data
        return session_response
```

この関数は `GenerativeAiAgentRuntimeClient.create_session` メソッドを実行し、それを返す関数です。

### end\_session
```python
class AgentRuntime:

    def end_session(self) -> None:
        self.oci_runtime_agent.delete_session(self.agent_endpoint_id, self.session_id)

```
この関数は、 `GenerativeAiAgentRuntimeClient.delete_session` メソッドを実行し、それを返す関数です。

### chat
```python:app.py
from oci.generative_ai_agent_runtime import GenerativeAiAgentRuntimeClient
from oci.generative_ai_agent_runtime.models import ChatResult

class AgentRuntime:

    def chat(self, message: str) -> ChatResult:
        chat_response: ChatResult = self.oci_runtime_agent.chat(
            agent_endpoint_id=self.agent_endpoint_id,
            chat_details=ChatDetails(
                user_message=message,
                should_stream=False,
                session_id=self.session_id,
            ),
        ).data
        self.chat_history.append(chat_response.message.content.text)
        return chat_response
```
この関数では、 `GenerativeAiAgentRuntimeClient.chat` メソッドを実行し、それを返します。
会話履歴保存用に、 `str` 型の `ChatResult.message.content.text` を追加しています。

### chat\_stream
```python:app.py
from oci.generative_ai_agent_runtime.models import ChatResult
from oci._vendor.sseclient import SSEClient

class AgentRuntime:

    def chat_stream(
        self, message: list
    ) -> Iterator[Union[Dict[str, List[Trace]], ChatResult]]:
        chat_stream_response: SSEClient = self.oci_runtime_agent.chat(
            agent_endpoint_id=self.agent_endpoint_id,
            chat_details=ChatDetails(
                user_message=message,
                should_stream=True,
                session_id=self.session_id,
            ),
        ).data

        for event in chat_stream_response.events():
            event_response = json.loads(event.data)
            if "message" in event_response:
                self.chat_history.append(event_response["message"]["content"]["text"])
                yield event_response
            else:
                yield event_response
```

`GenerativeAiAgentRuntimeClient.chat` メソッドをストリーミングで実行し、それを返す関数です。

会話履歴を保存するため、 `chat_stream` メソッド内部で SSE を辞書型へデコードしています。これにより、出力形式はイテレータとなっています。

つまり、出力タイプヒントの Union は厳密的に `Dict` 型であることに注意が必要です。（一応構造としては同じなはず）。 `ChatResult` 型や `Trace` 型インスタンスに変換していない理由は、そのデータクラスのセッターがまだ十分に整備されていないためです。今後に期待ということで妥協します。

# 実行！

```bash
$ User Input: 世帯主が万が一の事態により3ヶ月間入院した場合、必要な資金はいくら必要と考えられていますか？
$ Chat result: 世帯主が２～３カ月入院した場合に、必要と考える資金額は、月額24.2万円となっている。したがって、3ヶ月間入院した場合、必要な資金は24.2万円×3＝72.6万円となります。
$ User Input: ２ヶ月では何円？
$ Stream Chat result: 世帯主が２～３カ月入院した場合に、必要と考える資金額は、月額24.2万円となっている。したがって、２ヶ月では24.2万円×２＝48.4万円となります。
```

1つ目の質問の模範回答は以下のようなものでした。

> 世帯主が2～3ヶ月間入院する場合、健康保険診療の範囲外の費用として必要と考える資金額は月間24.2万円と考えられています。したがって、3ヶ月入院した場合は72.6万円（24.2万円×3ヶ月）が求められます。

生成結果と比べると、語尾や表現の差はありますが、問題なく情報を抽出・生成できていると言っても良いでしょう。

続いて、2つ目の質問の模範回答は以下のようなものでした。

>世帯主が2～3ヶ月間入院する場合、健康保険診療の範囲外の費用として必要と考える資金額は月間24.2万円と考えられています。したがって、2ヶ月入院した場合は48.4万円（24.2万円×2ヶ月）が求められます。


こちらの質問は「入院」などの単語を入れていないため、文脈が理解できていないと正確に答えることができない質問でした。こちらも語尾や表現の差はありますが、1問目と同様に問題ない結果と言えるでしょう。

結論として、ストリーミング出力をするときとしないとき両方の場合において問題なく動かせることが確認できました。また、精度についても問題なく、さらにセッション id での文脈の保持も確認することができました。

# まとめ

本記事では、まず初めに OCI Python SDK は Generative AI Agent に特化した主要クラス・メソッドを紹介しました。そして実際に SDK を使って RAG を呼び出すスクリプトを作成し、最後に RAG を実行してみました。

OCI Generative AI Agent の用途に限っても、そこにはかなりたくさんの関数が用意されていました。ここで触れられなかったもののほうが多いくらいです。また、紹介した関数についても省略した引数が数多く存在し、引数や設定項目の自由度の高さを実感できました。

特に、引数が豊富に用意されているため、簡潔なコードで素早く試すことも、細部までチューニングして作り込むこともできます。こうした柔軟性は、要件に合わせて開発スタイルを選びたい場面で大きな強みになるはずです。

この記事が何かのお役に立てれば幸いです。それでは、またどこかで会いましょう👋