Copilot StudioでDeepResearchを自作 & Deep Reasoning Models機能を試してみる

# はじめに
`AI`の進化が早すぎる。もはや私が言うまでもないことですが、本当に早い。
様々な便利な機能が生まれる中で、特に`DeepResearch`の機能が気に入っています。

`Gemini`、`Grok`、`ChatGPT`などで利用できますが、`o3`の`DeepResearch`を実行すると、もう私はブログを書く意味はないのでは🧐と感じるほどです、

とても気に入っておりChatGPTには`Pro`プランで加入しているものの、全く使用回数が足りません。`MAX`プランへの加入は、金銭的な課題で断念しています。

そこで今回は`DeepResearch`の機能を再現できないか、検証してみました。

# 今回登場する製品
今回は`Copilot Studio`そして`カスタムコネクタ`、`Azure Functions`を組み合わせて、`DeepResearch`の機能を再現してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/b3094747-c917-410d-8608-e5236863293f.png)

なぜ`Copilot Studio`を使ってみるのか、それは **Deep Reasoning Models機能**が利用できることから選定しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/95dd9f08-8350-4312-848c-937e3284bf2c.png)

https://learn.microsoft.com/en-us/microsoft-copilot-studio/authoring-reasoning-models

::: note info
本記事は `2025.04.20`時点の記事です。
内容は数日後に陳腐化する可能性があります
:::

`Copilot Studio`の**Deep Reasoning Models機能**は現在プレビュー段階の機能です。
展開されているリージョンも限定的であり、LLMモデルは` Azure OpenAI o1`となっています。

Copilot Studio内で生成 AI を使用し、アクション、トピック、ナレッジの組み合わせが実現できるため、どれほど使えるのか、試してみたいと思います。

# 実現までのステップ
`DeepResearch`の機能を再現する方法は`Dify`のテンプレートを参考にしています。

https://cloud.dify.ai/apps

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/b1b10595-cdcb-4a72-a03b-eda66e6363b9.png)

 1. トピックを`Copilot Studio`の機能で抽出する
 1. 調査するキーワードを`Gemini`で拡張する。モデル(gemini-2.0-flash)
 1. (2)の結果から[Tavily](https://tavily.com/)のSearch APIを使う
 1. (3)の結果のサマリから、`Copilot Studio`で`Deep Reasoning Models機能`を利用する

`Dify`では`DeepSeek R1`が使われていますが、個人的な好みと価格の魅力から`Gemini`を選択しています。
`Azure Functions`以外は、一定の無料枠がある破格のAPIです。

上記の手順のうち(2),(3)の工程は、`Azure Functions`でまとめて書いています。
Power Automateで書き上げることもできそうな内容ですが、AIのコーディング力を活用すると、`Azure Functions`で作ったほうが早いといえます。

また安さにこだわるので有れば`PythonAnywhere`や`Render`、`Cloudflare`、`Replit`といったサービスでも再現可能です。全部使ってみましたが、`PythonAnywhere`は非常に手軽、凝って作るのであれば`Cloudflare`という選択で決めてます。`Cloudflare`は無償枠が大きいため心配に感じるほどです。

https://www.pythonanywhere.com/

https://render.com/

https://www.cloudflare.com/ja-jp/

https://replit.com/

# カスタムコネクタ(Azure Functions)を準備する

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-overview?pivots=programming-language-python

```
 2. 調査するキーワードを`Gemini`で拡張する。モデル(gemini-2.0-flash)
 3. (2)の結果から[Tavily](https://tavily.com/)のSearch APIを使う
```

上記の部分は[Azure Functions](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-overview?pivots=programming-language-python)です。
内容は`Tavily`と`Gemini`のAPIを使っているだけなので割愛します。フルAIコーディングですが内容に相違はないです。

```python:function_app.py
import logging
import json
import azure.functions as func
import os
from typing import List, Dict, Any, Optional

# Import the official client libraries
import google.generativeai as genai
from tavily import TavilyClient

# Set up API keys from environment variables
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
TAVILY_API_KEY = os.environ.get("TAVILY_API_KEY")

# Configure the Google Generative AI client
genai.configure(api_key=GEMINI_API_KEY)

# Create Tavily client
tavily_client = TavilyClient(api_key=TAVILY_API_KEY)

# Define the function app
app = func.FunctionApp()

# Gemini prompt template embedded directly in the script
GEMINI_PROMPT_TEMPLATE = "引用で記載します"

def extract_search_terms(topic: str) -> List[str]:
    """Use Gemini API to extract search terms from a topic."""
    # Create the prompt for Gemini
    prompt = f"{GEMINI_PROMPT_TEMPLATE}\n\nResearch topic: {topic}"
    
    # Initialize the model
    model = genai.GenerativeModel('gemini-2.0-flash')
    
    # Generate content
    generation_config = {
        "temperature": 0.2,
        "top_p": 0.95,
        "top_k": 40
    }
    
    response = model.generate_content(
        prompt,
        generation_config=generation_config
    )
    
    generated_text = response.text
    logging.info(f"Gemini response: {generated_text}")
    
    # Extract the JSON from the generated text
    try:
        json_start = generated_text.find("{")
        json_end = generated_text.rfind("}") + 1
        
        if json_start == -1 or json_end == 0:
            logging.warning("No JSON found in Gemini response")
            return [topic]  # Return original topic if we can't parse JSON
        
        json_str = generated_text[json_start:json_end]
        result = json.loads(json_str)
        
        next_search_topic = result.get("nextSearchTopic")
        
        # Handle different types of nextSearchTopic
        if next_search_topic is None:
            return []
        elif isinstance(next_search_topic, str):
            return [next_search_topic]
        elif isinstance(next_search_topic, list):
            return next_search_topic
        else:
            logging.warning(f"Unexpected type for nextSearchTopic: {type(next_search_topic)}")
            return [topic]
    
    except Exception as e:
        logging.error(f"Error parsing Gemini response: {e}")
        return [topic]  # Return original topic if parsing fails

def search_with_tavily(term: str) -> Dict[str, Any]:
    """Use Tavily API to search for information."""
    # Use the Tavily client to perform the search
    response = tavily_client.search(
        query=term,
        search_depth="advanced",
        include_answer=True
    )
    
    return response

def structure_results(topic: str, search_results: Dict[str, Dict[str, Any]]) -> str:
    """Structure search results in markdown format."""
    markdown_content = f"# Deep Research: {topic}\n\n"
    
    # Summary section
    markdown_content += "## Research Summary\n\n"
    for term, result in search_results.items():
        if "answer" in result and result["answer"]:
            markdown_content += f"### {term}\n{result['answer']}\n\n"
    
    # Detailed findings section
    markdown_content += "## Detailed Findings\n\n"
    for term, result in search_results.items():
        markdown_content += f"### {term}\n\n"
        
        if "results" in result and result["results"]:
            for i, source in enumerate(result["results"]):
                markdown_content += f"#### Source {i+1}: {source.get('title', 'No Title')}\n"
                markdown_content += f"- URL: {source.get('url', 'No URL')}\n"
                
                if "snippet" in source and source["snippet"]:
                    markdown_content += f"- Snippet: {source['snippet']}\n\n"
                elif "content" in source and source["content"]:
                    content_snippet = source["content"][:200] + "..." if len(source["content"]) > 200 else source["content"]
                    markdown_content += f"- Snippet: {content_snippet}\n\n"
                else:
                    markdown_content += "\n"
    
    return markdown_content

def deep_research(topic: str) -> str:
    """Main function to perform deep research on a topic."""
    # Step 2: Extract search terms using Gemini API
    logging.info(f"Extracting search terms for topic: '{topic}'")
    search_terms = extract_search_terms(topic)
    
    if not search_terms:
        return f"# Deep Research: {topic}\n\nNo search terms were generated for this topic."
    
    logging.info(f"Generated search terms: {search_terms}")
    
    # Step 3: Search for each term using Tavily API
    search_results = {}
    
    for term in search_terms:
        logging.info(f"Searching for term: '{term}'")
        
        try:
            result = search_with_tavily(term)
            search_results[term] = result
        except Exception as e:
            logging.error(f"Error searching for term '{term}': {str(e)}")
            search_results[term] = {"results": [], "answer": f"Error: {str(e)}"}
    
    # Step 4: Structure the results in markdown format
    return structure_results(topic, search_results)

@app.route(route="deep_research", auth_level=func.AuthLevel.FUNCTION)
def deep_research_http_trigger(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    try:
        # Get the request body
        req_body = req.get_json()
        
        # Extract the search topic from the request
        if 'topic' in req_body:
            topic = req_body['topic']
        else:
            return func.HttpResponse(
                json.dumps({"error": "Please pass a 'topic' property in the request body"}),
                status_code=400,
                mimetype="application/json"
            )
        
        # Perform deep research
        research_results = deep_research(topic)
        
        # Return the results
        return func.HttpResponse(
            json.dumps({"research_results": research_results}),
            status_code=200,
            mimetype="application/json"
        )
        
    except ValueError as e:
        return func.HttpResponse(
            json.dumps({"error": f"Invalid request format: {str(e)}"}),
            status_code=400,
            mimetype="application/json"
        )
    except Exception as e:
        logging.error(f"Error processing request: {str(e)}")
        return func.HttpResponse(
            json.dumps({"error": f"Internal server error: {str(e)}"}),
            status_code=500,
            mimetype="application/json"
        )
```
markdownがぶれるのでGeminiのプロンプトは👇に書きます

```python
"""You are a research agent investigating the following topic.
What have you found? What questions remain unanswered? What specific aspects should be investigated next?

## Output
- Do not output topics that are exactly the same as already searched topics.
- If further information search is needed, set nextSearchTopic.
- If sufficient information has been obtained, set shouldContinue to false.
- Please output in json format

{こちらにjson schemae}
"""
```

```json
nextSearchTopic: str | None
shouldContinue: bool 
```


この`Azure Functions`の用途は`topic`を引数に`research_results`を返します。

今回登場している[Tavily](https://tavily.com/)はAIリサーチツールです。
月1,000 creditsが付与される`Free`プランもあります。
登録はクレジットカード不要です。

https://tavily.com/

https://docs.tavily.com/documentation/api-credits

また[Gemini](https://gemini.google.com/app?hl=ja)も同様ですね。

https://gemini.google.com/app?hl=ja

https://ai.google.dev/gemini-api/docs/quickstart?hl=ja&lang=python

`Gemini-2.0-flash`も無料枠が設けられており、非常に高性能なAIを気軽に使うことができます。

https://ai.google.dev/gemini-api/docs/pricing?hl=ja#gemini-2.0-flash-lite


さて`Azure Functions`の準備ができたら、`カスタムコネクタ`を用意します。
`Azure portal`には、非常に便利な機能として`OpenAPI`ファイルを出力する機能があります。

https://learn.microsoft.com/ja-jp/azure/api-management/export-api-power-platform

https://learn.microsoft.com/ja-jp/azure/azure-functions/function-keys-how-to?tabs=azure-portal

`Power Platform`と`プロコード`の融合が進む内容になりますね。
また後日カスタムコネクタの設定、`Azure Functions`の設定を含め、すべて`GitHub`にアップする予定です。
コピペで済ませたいかたはそちらをご参照ください。

# Copilot Studio側の設定
Copilot Studioの設定ですが、`Power Platform`の環境を`US`、プライマリ言語が`英語(en-us)`となっています。

■ Power Platformの環境
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/ff3a6433-fef0-4f48-9d99-de8738b834fa.png)

■ Copilot Studio
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/d4b1cbc6-7878-4bbc-af75-c6fd38d8be03.png)

>  重要
> 生成オーケストレーションは、プライマリ言語が英語 (en-US) のエージェントのみをサポートします。 他の言語はまだサポートされていません。
>> [生成 AI でエージェントの動作を調整する](https://learn.microsoft.com/ja-jp/microsoft-copilot-studio/advanced-generative-actions)

また課金へ影響が出る可能性があります。、ご注意ください。

> 生成オーケストレーションをオンにすると、課金の計算方法に影響を与える可能性があります。
>> [生成オーケストレーションの請求](https://learn.microsoft.com/ja-jp/microsoft-copilot-studio/requirements-messages-management)

プレビュー機能を使う上で非常に重要なことですので、自分の画面に機能が見当たらない場合は、環境や言語の設定を疑ってみてください。

https://learn.microsoft.com/ja-jp/microsoft-copilot-studio/advanced-generative-actions

この中で生成型アクションと`Deep Reasoning Models機能`をオンにします。

## 詳細セクション

Copilot Studioは自然言語で高機能なAIチャットボットが構築できる魅力的な製品です。
自分が期待する役割、指示は入念に検討し、設定する必要があります。

また`Deep Reasoning Models機能`を利用する上では、システムプロンプトである指示に`reason`という文字が入っていることが必須になります。

https://learn.microsoft.com/ja-jp/microsoft-copilot-studio/authoring-reasoning-models

```plaintext:説明
An AI-powered research assistant that performs comprehensive analysis on any topic. It extracts key search terms, gathers relevant information, and provides deep reasoning with structured summaries to help users quickly understand complex subjects.
```

```plaintext
I am a DeepResearch Assistant designed to help you explore topics in depth. I can analyze your requests, identify key research topics, search for relevant information, and provide reasoned analysis with comprehensive summaries.

When you give me a topic to research, I will:
- Extract the most relevant search terms
- reason: Gather information from reliable sources
- reason: Apply deep reasoning to analyze findings
- reason: Provide a structured summary with key insights
- Highlight areas for further exploration

My responses include both high-level summaries and detailed findings with source information. Feel free to ask follow-up questions about any aspect of my research.
```

## カスタム エージェントでアクションを使用する
Copilot Studioの期待大の機能として、現段階では`プレビュー`ですが、コア アクションを実施することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/3e93f455-7663-4746-b913-04bfccb62d75.png)

> 生成オーケストレーション をオンにすると、エージェントは、ユーザーに対応するために自動的に最も適切なアクションやトピックを選択したり、ナレッジを検索したりすることができます。

プロンプトから、トピックのみで引数を判別し
- 事前構築されたコネクタ アクション
- ユーザー定義コネクタとアクション
- Power Automate クラウド フロー
- AI Builder プロンプト (トピック内)
- Bot Framework スキル
- REST API 接続

上記を自律的に実行する機能です。入力にはエージェントの判断による`動的な値`や`変数`、`PowerFx`を用いることができる魅力的な機能です。
引数の設定が簡略化し、アクションを追加していくことでエージェントの拡張が見込めます。

今回は`カスタム コネクタ`なので`ユーザー定義コネクタとアクション`に該当します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/90c85bf5-8e1a-4e2d-9f76-5129a0b69a80.png)

プロンプトに書いた通り、トピックを判別し検索を実行、そのあと深い推論を実施します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/b94452a3-a376-4ecd-8c77-29de025d775c.png)

### Model Context Protocol (MCP) 
この機能については、若干`Model Context Protocol (MCP) `と似たところを私は感じました。
今話題の`モデルコンテキストプロトコル (MCP)`ですが、クライアントである`Claude Desktop`から`MCPサーバー`にあるツールをAIで判別し、ツールを実行します。

https://modelcontextprotocol.io/introduction

ハードルの低さも魅力です。私自身もPython SDKを使い、SharePointの操作を自動化するMCPサーバーを設定することができました。

https://github.com/DEmodoriGatsuO/sharepoint-mcp

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">SharePoint MCP...<br>流行りにのって、SDKからClaudeの力を存分に借りてMCPツールを作ってみました！<br><br>自然言語からSharePointサイトを操っています🐟<br>デモではListsを作成したり、内容を提案したり、ドキュメントライブラリをホストして資料を格納したりと盛りだくさんです。<a href="https://twitter.com/hashtag/Claude?src=hash&amp;ref_src=twsrc%5Etfw">#Claude</a> <a href="https://twitter.com/hashtag/MCP?src=hash&amp;ref_src=twsrc%5Etfw">#MCP</a> <a href="https://t.co/I7aVd8HCne">pic.twitter.com/I7aVd8HCne</a></p>&mdash; 出戻りガツオ🐟 Microsoft MVP (@DemodoriGatsuo) <a href="https://twitter.com/DemodoriGatsuo/status/1910297504119501309?ref_src=twsrc%5Etfw">April 10, 2025</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

爆発的にMCPサーバーは広がっており、`Azure`でもMCPが発表されています。

また自分で建てることもできます。

https://techcommunity.microsoft.com/blog/appsonazureblog/build-ai-agent-tools-using-remote-mcp-with-azure-functions/4401059

`Copilot Studio`と`モデルコンテキストプロトコル`の連動も実はできたりするのですが、非常に難しく反応しません。コミュニティもできているので、できた方はぜひ寄稿してみてください。

https://community.powerplatform.com/forums/thread/?discussionforumid=93e48e36-a110-f011-9989-6045bdeb8a5d

# お手製`DeepResearch`の実力やいかに！

```plaintext:プロンプト
モデルコンテキストプロトコル(MCP)について日本語で深い推論も加えて内容を説明してください
```

Deep Reasoning Models機能が使われると目に見えて、リサーチに時間をかけてもらえます！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/064c13fd-ada9-49e6-a2f6-14f1e2706b15.png)

それっぽい！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/0df3fb72-cd23-4e33-a3e4-28ef12aa4428.png)


使ってみた感想ですが、`o3`など本家のモデルには到底及びません。`Perplexity`もありますが、意外とお金かかるんですよね。いい方法が知りたい。