Grounding with Bing Search のグラウンディング結果をフロントエンドに表示するためのTips

# はじめに

Azure AI Agent Service から Grounding with Bing Search を使用する場合、以下の利用規則 (Terms of Use / ToU) の通り、フロントエンドに Web サイトの URL と Bing 検索クエリ URL の両方を表示する必要があります。

>Grounding with Bing Search allows you to leverage the Content of the Bing Search APIs as grounding data for your Application. After you enable the Grounding with Bing Search functionality in the Azure AI Agent Service, the Azure AI Agent will call the appropriate Bing Search APIs, process the results of the API call, and then provide you with an output which includes parts of the Content (the “Answer”). The Answer will include citations (with hyperlinks to Web Results) and a link to the Bing Search Results Page with the query sent to Bing Search API (together, these are “References”). You must retain and display the References in the exact form provided by Microsoft.

（日本語訳）
>「Grounding with Bing Search」は、Bing Search API のコンテンツを、アプリケーションの根拠データとして活用できる機能です。Azure AI Agent Service で「Grounding with Bing Search」機能を有効にすると、Azure AI Agent は適切な Bing Search API を呼び出し、その API 呼び出しの結果を処理した上で、コンテンツの一部を含む出力（「回答」）を提供します。この回答には、Web 結果へのハイパーリンク付きの引用情報と、Bing Search API に送信されたクエリを含む Bing 検索結果ページへのリンク（合わせて「参照情報」と呼ばれる）が含まれます。参照情報は、Microsoft によって提供された正確な形式で保持し、表示する必要があります。

https://www.microsoft.com/en-us/bing/apis/grounding-legal#use-and-display-requirements

Docs にも以下のように記載があります。

>Bing を使用したグラウンディングの使用条件と利用と表示の要件によると、 カスタム インターフェイスに Web サイトの URL と Bing 検索クエリ URL の両方を表示する必要があります。 Web サイトの URL は API 応答の annotations パラメーターを通じて検索でき、Bing 検索クエリ URL は runstep の詳細を通じて検索できます。 Web ページをレンダリングするには、Bing 検索クエリ URL のエンドポイントを www.bing.com に置き換えることをお勧めします。Bing 検索クエリ URL は "https://www.bing.com/search?q={search クエリ}" のように表示されます。

https://learn.microsoft.com/ja-jp/azure/ai-services/agents/how-to/tools/bing-grounding?tabs=python&pivots=overview#how-to-display-grounding-with-bing-search-results

あえて噛み砕いてみると、 **Azure AI Agent Service に Grounding with Bing Search を組み込む場合、フロントエンドの画面には、【ソース URL】【クエリ文字列を含む Bing URL】を表示する必要がある**ということです。

以下は Docs に添付されている画面ショットを一部加工したものですが、開発時には、これらを必ずフロントエンドに表示することが義務付けられているのです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248756/af81e02c-9d65-4d46-936d-4f53f4e51fc8.png)

# ソース URL の取得方法

ソース URL は、AI Agent Service からのレスポンスに含まれています。

以下は、AI Agent Service からの回答に加え、ソース URL も併せて取得する処理の一例です。

```python:get_assistant_response_with_source_url.py
# アシスタントが生成した回答文を格納するリスト
assistant_messages = []

# ソース URL を格納するリスト
url_citations = []

agent_messages: OpenAIPageableListOfThreadMessage = self.project_client.agents.list_messages(thread_id=thread.id)

for data_point in reversed(agent_messages.data):
    last_message_content = data_point.content[-1]
    if not isinstance(last_message_content, MessageTextContent):
        continue

    if data_point.role != MessageRole.AGENT:
        continue

    assistant_messages.append(last_message_content.text.value)

    for annotation in last_message_content.text.annotations:
        if annotation.type != 'url_citation':
            continue

        url = annotation.get("url_citation", {}).get("url")
        if url:
            url_citations.append(url)
```

大まかな処理の流れは下記の通りです。

1.  `agent_messages.data` に格納されたメッセージを逆順（最新のメッセージから順番）に処理します。
2. 各メッセージの最後のコンテンツ（`last_message_content`）を取得します。
3. 取得したコンテンツがテキスト形式（`MessageTextContent`）でない場合はスキップします。
4. メッセージの役割（`data_point.role`）がエージェント（`MessageRole.AGENT`）でない場合もスキップします。
5. 上記の条件を満たした場合、アシスタントの回答文を`assistant_messages`リストに追加します。
6. 回答文に含まれるアノテーション（`annotations`）を順番に確認し、タイプが`url_citation`であるものを抽出します。
7. `url_citation`タイプのアノテーションからURLを取得し、存在する場合は`url_citations`リストに追加します。
8. 結果として、アシスタントが生成した回答文（`assistant_messages`）と、それに関連する引用元URL（`url_citations`）が抽出されます。


以下は、「マイクロソフトの株価」というクエリを投げた際のレスポンス例です。辞書型で返ってくるので、`annotation.get("url_citation", {}).get("url")` で `url` キーに該当する値を取得するところが肝となります。

```json
{
  'type': 'text', 
  'text': {
    'value': '最新のマイクロソフト（Microsoft）の株価は、393.31ドルであり、前日比で0.90%下落しています【3†source】。マイクロソフトの株価情報や企業概要、業績指標についての詳細はYahoo Financeで確認できます。', 
    'annotations': [
      {
        'type': 'url_citation', 
        'text': '【3†source】', 
        'start_index': 54, 
        'end_index': 64, 
        'url_citation': {
          'url': 'https://markets.businessinsider.com/news/stocks/new-buy-rating-for-microsoft-msft-the-technology-giant-1034458887', 
          'title': 'New Buy Rating for Microsoft (MSFT), the Technology Giant'
        }
      }
    ]
  }
}
```

# クエリ文字列を含む Bing URL の取得方法

クエリ文字列は、`run_step` オブジェクトの詳細を辿ることで取得可能です。この `run_step` オブジェクトは、`project_client.agents.list_run_steps` 関数を実行することで取得可能です。

```python:get_bing_url_with_query_string.py
run_steps: OpenAIPageableListOfRunStep = project_client.agents.list_run_steps(run_id=run.id, thread_id=thread.id)
run_steps_data = run_steps['data']

# Extract 'q' parameter from requesturl in run steps
for step in run_steps_data:
    if step['type'] != 'tool_calls':
        continue
        
    for tool_call in step['step_details']['tool_calls']:
        if 'bing_grounding' not in tool_call:
            continue
            
        request_url = tool_call['bing_grounding']['requesturl']
        parsed_url = urlparse(request_url)
        query_params = parse_qs(parsed_url.query)
        
        if 'q' in query_params:
            # URLデコードして検索クエリを取得
            query = unquote(query_params['q'][0])
            break  # クエリを見つけたらループを終了
            
    # クエリが見つかったら外側のループも終了
    if query:
        break
```

大まかな処理の流れは以下の通りです。

1. AI の実行ステップから `tool_calls` タイプのものだけを処理対象にし、それ以外のステップは `continue` でスキップします。
2. 各ツール呼び出しから `bing_grounding` を含むもののみを処理します。
2. 検索リクエスト URL を取得し、`urlparse` と `parse_qs` で解析することで、URL のクエリパラメータを抽出します。
2. `q` パラメータから検索クエリを取得し、`unquote` 関数で日本語などのURLエンコード文字を正しくデコードします。

## フロントエンドでクエリ文字列を含む Bing URL を表示するための注意点

さて、フロントエンドで【クエリ文字列を含む Bing URL】を表示するためには、一つ注意すべき点があります。それは、**Bing 検索クエリ URL のエンドポイントを `www.bing.com` に置き換えること**です。

実際のレスポンス例を見ながら確認します。
上記の Python コードでは、以下のレスポンス内の `requesturl` に含まれている値 (例: `https://api.bing.microsoft.com/v7.0/search?q="Microsoft stock price"`) のクエリ文字列 `Microsoft stock price` を取得しています。

```json
[{
  'id': 'step_Oay6qEDQ11as1bGsOfvdwSl4', 
  'object': 'thread.run.step', 
  'created_at': 1741623611, 
  'run_id': 'run_d2w19Y9GyqNljoSdRUmFDKPN', 
  'assistant_id': 'asst_UA0ifgat91AKtIjgQ8UA9eM2', 
  'thread_id': 'thread_oXHo6u1nWZswKt5PuEFsicui', 
  'type': 'message_creation', 
  'status': 'completed', 
  'cancelled_at': None, 
  'completed_at': 1741623612, 
  'expires_at': None, 
  'failed_at': None, 
  'last_error': None, 
  'step_details': {
    'type': 'message_creation', 
    'message_creation': {
      'message_id': 'msg_My3Bel0ppjwuCSnkO3T63lFJ'
    }
  }, 
  'usage': {
    'prompt_tokens': 2556, 
    'completion_tokens': 101, 
    'total_tokens': 2657, 
    'prompt_token_details': {
      'cached_tokens': 0
    }
  }
}, 
{
  'id': 'step_738JHhKNchje0mXItU3SPzuo', 
  'object': 'thread.run.step', 
  'created_at': 1741623610, 
  'run_id': 'run_d2w19Y9GyqNljoSdRUmFDKPN', 
  'assistant_id': 'asst_UA0ifgat91AKtIjgQ8UA9eM2', 
  'thread_id': 'thread_oXHo6u1nWZswKt5PuEFsicui', 
  'type': 'tool_calls', 
  'status': 'completed', 
  'cancelled_at': None, 
  'completed_at': 1741623611, 
  'expires_at': None, 
  'failed_at': None, 
  'last_error': None, 
  'step_details': {
    'type': 'tool_calls', 
    'tool_calls': [
      {
        'id': 'call_KkzVbb8TJhVG74fRw0IXb4Mb', 
        'type': 'bing_grounding', 
        'bing_grounding': {
          'requesturl': 'https://api.bing.microsoft.com/v7.0/search?q="Microsoft stock price"'
        }
      }
    ]
  }, 
  'usage': {
    'prompt_tokens': 473, 
    'completion_tokens': 12, 
    'total_tokens': 485, 
    'prompt_token_details': {
      'cached_tokens': 0
    }
  }
}]
```

フロントエンド Bing 検索クエリ URL のエンドポイントを `www.bing.com` に置き換えることで、当該クエリパラメータをキーワードとしてフロントエンドから検索する UI を簡単に実現可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/248756/bc1ab1df-6c73-4142-af08-bc5a7b8d37e2.png)

# おわりに

Azure AI Agent Service から Grounding with Bing Search を使用する際の利用規則に則ったコードを書くためのヒントとなれば幸いです。

## 参考

https://learn.microsoft.com/ja-jp/azure/ai-services/agents/how-to/tools/bing-grounding?tabs=python&pivots=overview#how-to-display-grounding-with-bing-search-results