Azure AI Agent ServiceとSemantic Kernel で Agentic RAGを試す

Agentic RAGが気になったので、Azure AI Agent ServiceとSemantic Kernel で実装してみました。基本的に以下の内容に従っています。

https://github.com/microsoft/ai-agents-for-beginners/blob/main/05-agentic-rag/README.md

https://github.com/microsoft/ai-agents-for-beginners/blob/main/05-agentic-rag/code_samples/05-semantic-kernel-azure-ai-agent.ipynb

私が実行したプログラムはこちら。

https://github.com/YoheiFukuhara/azure-ai/blob/main/agent/rag-agent/05-semantic-kernel-azure-ai-agent.ipynb


# 処理
## 0. 前提
Azure AI Foundryの基本的なリソースはAzure Portalから作成済です。AI Projectも作成済。その中で(gpt-4.1.-miniでやろうとしたら、プログラム上でエラー発生したので)gpt-4o-miniをデプロイ。
SQLiteがコメントアウトされてコードにありますが、特に使っていません。

### 環境
最初はWindowsで動かそうとしたのですが、Pythonパッケージ`azure-ai-projects`が古くて動かなかったので、WSLで動かしています。

|種類|バージョン|備考|
|:-:|:-:|:-:|
|Python(Pyenv)|3.10.12|古いのに特に意味はないです。たまたますぐ使えた環境
|Ubuntu(WSL2)|22.04.5 LTS|WSL
|Poetry|1.7.1|

### Python 追加パッケージ

|種類|バージョン|備考|
|:-:|:-:|:-:|
|azure-ai-projects|1.0.0b10|
|azure-ai-inference|1.0.0b9|
|azure-identity|1.21.0|
|azure-monitor-opentelemetry|1.6.8|トレース出力目的
|jupyterlab|4.4.1|
|semantic-kernel|1.29.0|
|python-dotenv|1.1.0|

## 1. 事前準備
### 1.1. RAG用ファイル作成
ローカルファイル`document.md`を置いておきます。このファイルがRAGで使われます。

```markdown: document.md
- **Contoso Travel** offers luxury vacation packages to exotic destinations worldwide.
- Our premium travel services include personalized itinerary planning and 24/7 concierge support.
- Contoso's travel insurance covers medical emergencies, trip cancellations, and lost baggage.
- Popular destinations include the Maldives, Swiss Alps, and African safaris.
- Contoso Travel provides exclusive access to boutique hotels and private guided tours.
```

https://github.com/YoheiFukuhara/azure-ai/blob/main/agent/rag-agent/document.md

### 1.2. 環境変数設定
`.env`に環境変数を設定しておきます。

```: .env
AZURE_AI_AGENT_PROJECT_CONNECTION_STRING=プロジェクト接続文字列
AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME=gpt-4o-mini
AZURE_TRACING_GEN_AI_CONTENT_RECORDING_ENABLED=true
```

こちらの内容を参考にしています。

https://learn.microsoft.com/ja-jp/semantic-kernel/frameworks/agent/azure-ai-agent?pivots=programming-language-python#configuring-the-ai-project-client

## 2. Python Script
### 2.1. パッケージインポート

```python: パッケージインポート
import os

from azure.ai.projects import AIProjectClient
from azure.ai.projects.models import FileSearchTool, OpenAIFile, VectorStore
from azure.identity import DefaultAzureCredential
from azure.monitor.opentelemetry import configure_azure_monitor
from dotenv import load_dotenv
from semantic_kernel.agents import AzureAIAgent, AzureAIAgentThread
```

### 2.2. トレースON
以下を参考にトレースONにしました。単純なChatと違ってAgentは自動で処理が走るので、トレースで動きを確認します。

https://zenn.dev/tomodo_ysys/articles/azure-ai-foundry-llm-tracing

https://learn.microsoft.com/ja-jp/azure/ai-foundry/concepts/trace

```python: 
load_dotenv(override=True)
project_client = AIProjectClient.from_connection_string(
    credential=DefaultAzureCredential(), conn_str=os.environ["AZURE_AI_AGENT_PROJECT_CONNECTION_STRING"]
)

application_insights_connection_string = project_client.telemetry.get_connection_string()
if not application_insights_connection_string:
    print("Application Insights was not enabled for this project.")
    print("Enable it via the 'Tracing' tab in your Azure AI Foundry project page.")
    exit()
    
configure_azure_monitor(connection_string=application_insights_connection_string)
```

### 2.3. 主処理
主処理は大きく以下の流れ。

1. Agentの作成
    1. ファイルアップロード
    1. Vector Storeの作成
    1. Agentの作成
1. Agentの実行
1. Agent削除
    1. Vector Storeの削除
    1. ファイルの削除
    1. Agentの削除

```python: 
from azure.identity.aio import DefaultAzureCredential

async def main():
    async with (
        DefaultAzureCredential() as creds,
        AzureAIAgent.create_client(credential=creds) as client,
    ):
        file: OpenAIFile = await client.agents.upload_file_and_poll(file_path="document.md", purpose="assistants")
        vector_store: VectorStore = await client.agents.create_vector_store_and_poll(
            file_ids=[file.id], name="my_vectorstore"
        )

        # Define agent name and instructions tailored for RAG.
        AGENT_NAME = "RAGAgent"
        AGENT_INSTRUCTIONS = """
        You are an AI assistant designed to answer user questions using only the information retrieved from the provided document(s).

        - If a user's question cannot be answered using the retrieved context, **you must clearly respond**: 
        "I'm sorry, but the uploaded document does not contain the necessary information to answer that question."
        - Do not answer from general knowledge or reasoning. Do not make assumptions or generate hypothetical explanations.
        - Do not provide definitions, tutorials, or commentary that is not explicitly grounded in the content of the uploaded file(s).
        - If a user asks a question like "What is a Neural Network?", and this is not discussed in the uploaded document, respond as instructed above.
        - For questions that do have relevant content in the document (e.g., Contoso's travel insurance coverage), respond accurately, and cite the document explicitly.

        You must behave as if you have no external knowledge beyond what is retrieved from the uploaded document.
        """

        
        # Create file search tool with uploaded resources
        file_search = FileSearchTool(vector_store_ids=[vector_store.id])

        # 3. Create an agent on the Azure AI agent service with the file search tool
        agent_definition = await client.agents.create_agent(
            model="gpt-4o-mini",  # This model should match your Azure OpenAI deployment.
            name=AGENT_NAME,
            instructions=AGENT_INSTRUCTIONS,
            tools=file_search.definitions,
            tool_resources=file_search.resources,
        )
        
        # Create the Azure AI Agent using the client and definition.
        agent = AzureAIAgent(
            client=client,
            definition=agent_definition,
        )
        
        # Create a thread to hold the conversation
        # If no thread is provided, a new thread will be
        # created and returned with the initial response
        thread: AzureAIAgentThread | None = None
        
        # Example user queries.
        user_inputs = [
            "Can you explain Contoso's travel insurance coverage?",  # Relevant context.
            "What is a Neural Network?"  # No relevant context from the document. Will not contain a source annotation.
        ]
        
        try:
            for user_input in user_inputs:
                print(f"# User: '{user_input}'")
                # Invoke the agent for the specified thread for response
                async for response in agent.invoke(messages=user_input, thread=thread):
                    print(f"# {response.name}: {response}")
                    thread = response.thread
        finally:
            # Clean up resources.
            await thread.delete() if thread else None
            await client.agents.delete_vector_store(vector_store.id)
            await client.agents.delete_file(file.id)
            await client.agents.delete_agent(agent.id)
            print("\nCleaned up agent, thread, file, and vector store.")

await main()
```

```: 実行結果ターミナル
# User: 'Can you explain Contoso's travel insurance coverage?'
# RAGAgent: I'm sorry, but the uploaded document does not contain the necessary information to answer that question.
# User: 'What is a Neural Network?'
# RAGAgent: I'm sorry, but the uploaded document does not contain the necessary information to answer that question.

Cleaned up agent, thread, file, and vector store.
```


# Agentの定義など
## Agent 定義
Azure AI Foundryで作成されたAgentを見てみます(Agent削除処理をコメントアウトして実行)。
画面の右側に「手順」や「ナレッジ」があるのがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/e0cca9c4-40ce-4922-ba66-452a9d48970b.png)

ちなみに、BASICエージェントの場合、ナレッジは「ファイル検索」で別途Azure AI Searchなどのリソースが作成されるわけではなく、Microsoftマネージドの検索リソースが使われるようです。Standardエージェントだと、以下の通り。

> ファイル検索ツールは、利用者がエージェントのセットアップ中に接続した Azure AI 検索リソースと Azure Blob Storage リソースを使用します。
> - アップロードされたファイルは、接続された Azure Blob Storage アカウント内に保存されます
> - 接続された Azure AI 検索リソースを使用してベクトル ストアが作成されます

https://learn.microsoft.com/ja-jp/azure/ai-services/agents/how-to/tools/file-search?tabs=python&pivots=overview

## スレッド
スレッドが以下のように記録されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/e30439d4-b775-4d98-a662-d1218871c281.png)

## トレース
全体的に以下のトレースが取れています。Agent作成や削除なども含んでいます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/562797ef-0b68-4978-b8fb-b4c1d3d4d830.png)

実際にAgentをInvokeしているトーレス。outputやvector storeからの検索内容は見えないです。トレースに含める方法があるかは調べていません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134273/9deaa3e7-97c5-460f-a068-294ecfd31af4.png)