Databricks AIエージェントにおけるリソース認証：ユーザ代理認証を試す

:::note warn
今回試すDatabricksの機能はまだベータ版です。
将来的に動作が大きく変更になる可能性がありますので、ご注意ください。
:::


# 導入

DatabricksでAIエージェントを作成する際、利用するリソースの認証をどうするかは悩みの種です。
例えばDatabricksのリソース(Unity Catalog上のテーブルやGenie APIなど)や外部APIを利用する際、固定の権限で実行するのではなく、AIエージェントを利用するエンドユーザの権限で行いたい時があります。

Databricksでそれを簡単にしようと思ったらどうするのがいいんだろう、と思ってドキュメントを読んでいたら「ユーザ代理認証」の機能が追加されていました。

以下のドキュメントより、抜粋。

https://docs.databricks.com/aws/ja/generative-ai/agent-framework/log-agent#databricks-%E3%83%AA%E3%82%BD%E3%83%BC%E3%82%B9%E3%81%AE%E8%AA%8D%E8%A8%BC

> # Databricks リソースの認証
> AIエージェントは、タスクを完了するために他のリソースに対して認証する必要があることがよくあります。 たとえば、エージェントは、非構造化データをクエリするためにベクトル検索インデックスにアクセスする必要がある場合があります。
> 
> 依存リソースの認証で説明されているように、モデルサービングは、エージェントをデプロイするときに、Databricksマネージド リソースと外部リソースの両方に対する認証をサポートします。
> 
> モデルサービングは、 Databricks管理リソースに対して 2 種類の認証をサポートしています。
> 
> 1. **システム認証:** エージェント サービスプリンシパルが、エージェントのログ時に指定された任意の依存リソースにアクセスできるようにします。 これは、共有リソースや機密性の低いリソース(公開ドキュメントを含むベクトル検索インデックスなど)にアクセスする場合に便利です
> 2. **[ベータ版] ユーザー代理認証:** エージェントがエンド ユーザーの資格情報を使用して Databricks リソースにアクセスできるようにします。これは、エージェントが機密データにアクセスしたり、リモート APIにクエリを実行してユーザーごとにアクションを実行したりする必要があるシナリオに役立ちます

2025年4月現在、まだベータ版のようですが、エンドユーザの資格情報を使ってDatabricksリソースにアクセスできるようです。まさに求めている機能。

実際に挙動を確かめたく、以下の公式サンプルをもとにAIエージェントを作って試してみます。

https://docs.databricks.com/aws/ja/notebooks/source/generative-ai/agents-obo-example.html

ただ、そのまま動かしても（個人的に）面白くないので、公式サンプルはVector Searchでの対応をしていますが、今回はGenie Spaceに対応してみます。

検証はDatabricks on AWSで行いました。
ノートブックはサーバレスクラスタで動かしています。

# Step1. 準備

本機能はベータ版につき標準だと有効化されていないようなので、ワークスペースのプレビュー機能より有効化してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168727/272c97d2-7da5-44a9-9b50-2f6f32bdd05c.png)


次にノートブックを作成し、AIエージェント作成に必要なパッケージをインストールします。

```python
%pip install -U -qqqq mlflow langgraph==0.3.34 langgraph-supervisor databricks-langchain databricks-agents uv

%restart_python
```

次にサンプルとしてGenie Spaceと連携するAIエージェント作成します。
（複数Genieスペースに拡張できるよう```langgraph-supervisor```を利用しているため、少し冗長になっています)

今回は[こちらの記事](https://qiita.com/isanakamishiro2/items/e2747f35e88eb6f784d6#genie%E3%82%B9%E3%83%9A%E3%83%BC%E3%82%B9%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B)でサンプル用に作成した売上データ探索Genieスペースを再利用しています。

```python
%%writefile obo_agent.py
from typing import Any, Generator, Optional
import uuid

import mlflow
from databricks.sdk import WorkspaceClient
from databricks.sdk.credentials_provider import ModelServingUserCredentials
from databricks_langchain import ChatDatabricks
from databricks_langchain.genie import GenieAgent
from langgraph_supervisor import create_supervisor
from langchain_core.messages import BaseMessage, convert_to_openai_messages

from mlflow.pyfunc import ChatAgent
from mlflow.types.agent import (
    ChatAgentChunk,
    ChatAgentMessage,
    ChatAgentResponse,
    ChatContext,
)

mlflow.langchain.autolog()

###################################################
## Genie Spaceのエージェント作成用のパラメータ
###################################################

GENIE_SPACE_ID = "01f000e7c3141060b962a5b99f980efd" # 売上探索GenieスペースのID
genie_agent_description = "このエージェントは売上情報に関する質問に答えることができます。"

############################################
# LLMエンドポイントとシステムプロンプトを定義
############################################
LLM_ENDPOINT_NAME = "databricks-claude-3-7-sonnet"
llm = ChatDatabricks(endpoint=LLM_ENDPOINT_NAME)

supervisor_prompt = "あなたは売上などの経営データに関する専門家です。基本的にGenieエージェントを使ってください。"


def create_genie_agents():
    """Genieエージェントを作成する"""

    user_authenticated_client = WorkspaceClient(
        credentials_strategy=ModelServingUserCredentials()
    )
    genie_agents = []
    try:
        genie_agent = GenieAgent(
            genie_space_id=GENIE_SPACE_ID,
            genie_agent_name="Genie",
            description=genie_agent_description,
            client=user_authenticated_client,
        )
        genie_agent.name = "sales_genie_agent"
        genie_agents.append(genie_agent)
    except Exception as e:
        print(f"Genieエージェントツールをスキップ: {e}")

    return genie_agents


def convert_lc_message_to_chat_message(lc_message: BaseMessage) -> ChatAgentMessage:
    """LangChainメッセージをChatAgentMessageに変換する"""
    msg = convert_to_openai_messages(lc_message)
    if not "id" in msg:
        msg.update({"id": str(uuid.uuid4())})

    return ChatAgentMessage(**msg)


class LangGraphChatAgent(ChatAgent):
    def predict(
        self,
        messages: list[ChatAgentMessage],
        context: Optional[ChatContext] = None,
        custom_inputs: Optional[dict[str, Any]] = None,
    ) -> ChatAgentResponse:
        # ここでpredict呼び出し時にエージェントを初期化
        workflow = create_supervisor(
            create_genie_agents(),
            model=llm,
            prompt=supervisor_prompt,
        ).compile()
        request = {"messages": self._convert_messages_to_dict(messages)}

        messages = []
        for event in workflow.stream(request, stream_mode="updates"):
            for node_data in event.values():
                print(node_data)
                messages.extend(
                    convert_lc_message_to_chat_message(msg)
                    for msg in node_data.get("messages", [])
                )
        return ChatAgentResponse(messages=messages)

    def predict_stream(
        self,
        messages: list[ChatAgentMessage],
        context: Optional[ChatContext] = None,
        custom_inputs: Optional[dict[str, Any]] = None,
    ) -> Generator[ChatAgentChunk, None, None]:
        workflow = create_supervisor(
            create_genie_agents(),
            model=llm,
            prompt=supervisor_prompt,
        ).compile()

        request = {"messages": self._convert_messages_to_dict(messages)}
        for event in workflow.stream(request, stream_mode="updates"):
            for node_data in event.values():
                yield from (
                    ChatAgentChunk(**{"delta": convert_lc_message_to_chat_message(msg)})
                    for msg in node_data["messages"]
                )


# エージェントオブジェクトを作成し、mlflow.models.set_model()を介して推論のためにエージェントオブジェクトとして指定
AGENT = LangGraphChatAgent()
mlflow.models.set_model(AGENT)
```

動かすと以下のような結果がMLflow Tracingで得られます。

```python
from obo_agent import AGENT

AGENT.predict({"messages": [{"role": "user", "content": "先月の売上合計を教えて"}]})
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168727/f4787175-3bcb-423a-b12b-ec18b1db74be.png)


# Step2. エージェントのロギング

Step1で定義したエージェントをMLflowにロギングします。
**ここが今回のポイント。（解説？はコード後）**

```python
import mlflow
from obo_agent import LLM_ENDPOINT_NAME
from mlflow.models.resources import DatabricksServingEndpoint
from mlflow.models.auth_policy import AuthPolicy, SystemAuthPolicy, UserAuthPolicy
from pkg_resources import get_distribution

resources = [DatabricksServingEndpoint(endpoint_name=LLM_ENDPOINT_NAME)]
systemAuthPolicy = SystemAuthPolicy(resources=resources)

userAuthPolicy = UserAuthPolicy(
    api_scopes=[
        "serving.serving-endpoints",
        "dashboards.genie",
    ]
)
with mlflow.start_run():
    logged_agent_info = mlflow.pyfunc.log_model(
        artifact_path="obo_agent",
        python_model="obo_agent.py",
        extra_pip_requirements=[
            f"databricks-connect=={get_distribution('databricks-connect').version}"
        ],
        # auth_policyでSystem Auth PolicyとUser Auth Policyを指定。ここが今回の重要ポイント。
        auth_policy=AuthPolicy(
            system_auth_policy=systemAuthPolicy, user_auth_policy=userAuthPolicy
        ),
    )
```

簡単な解説ですが、```log_model```でモデルを保管する際に、```auth_policy```という珍しい？パラメータを指定しています。
ここでsystem_auth_policy(従来のresourcesに相当するパラメータ)とuser_auth_policyを指定します。
エンドユーザの資格情報を使う場合、user_auth_policyに適切なapi_scopeを指定する必要があります。
逆に言えば、ここのapi_scopeを設定すれば、エンドユーザの資格情報を使って各APIに対して実行できるようになる、ということです。

https://docs.databricks.com/aws/ja/generative-ai/agent-framework/log-agent#-%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E4%BB%A3%E7%90%86%E8%AA%8D%E8%A8%BC

以下のドキュメントにも記載があるのですが、利用には若干のリスクや考慮点があります。
注意して使いましょう。

https://docs.databricks.com/aws/ja/generative-ai/agent-framework/author-agent#%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E4%BB%A3%E7%90%86%E8%AA%8D%E8%A8%BC

ひとまずここまででエージェントをロギングすることができます。

# Step3. デプロイする

Mosaic AI Agent Frameworkを使ってエンドポイントにエージェントをデプロイします。
(本来は事前検証などするべきですが、説明が長くなるので割愛します）

まずは先ほどロギングしたエージェントをUnity Catalogに登録。

```python
mlflow.set_registry_uri("databricks-uc")

catalog = "training"
schema = "llm"
model_name = "obo_sample_agent"
UC_MODEL_NAME = f"{catalog}.{schema}.{model_name}"

# register the model to UC
uc_registered_model_info = mlflow.register_model(
    model_uri=logged_agent_info.model_uri, name=UC_MODEL_NAME
)
```

次にAgent Frameworkを使ってUnity Catalogに登録したモデルをデプロイ。

```python
from databricks import agents

agents.deploy(
    UC_MODEL_NAME,
    uc_registered_model_info.version,
    tags={"endpointSource": "docs"},
    scale_to_zero=True,
)
```

これで数分後にエージェントのデプロイが完了します。

では、実際に使ってみましょう。

# Step4. Playgroundで試す

作成したエージェントをPlayground上で試してみます。

軽い挨拶から。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168727/3bacb1bb-2a14-4f3b-84a1-18006ce4cf81.png)

この時点ではGenie Agentを呼び出してないのでまだ実行者の資格が使われているかどうかわかりません。

というわけで、売上の集計を聞いてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168727/e59bab10-e2ca-408e-87c7-841ff5b570f1.png)

実行結果は以下のようになります。（会話履歴長いので最後の一部のみ）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168727/5060b39f-c5fd-4132-8fcd-662971b766fc.png)

というわけでちゃんとGenie Spaceにアクセスした上で結果を得ることができました。

もし、Genie Spaceに対する資格情報が無い場合は通信できないはずなので、正しくユーザ代理認証による資格情報が利用されていることになります。

## おわりに

ユーザ代理認証を使ってAIエージェントからDatabricksのサービスを利用してみました。
Unity Catalogの接続オブジェクトも対応しているため、実質外部APIサービスの利用も代理認証の資格情報を使って呼び出すことができるのではないかと思います。

AIエージェントでMCPを使う場合、認証系どうしようと考えることが増えてきているのですが、代理認証の仕組が実装されることでこのあたりも解消するかもしれません。今度試してみようと思います。

まだベータ版ではありますので、早期にGAまで進むことを期待しています。