Azure OpenAIとPower Automate、LINEで快適な記事収集ツールを作ってみた

# はじめに
世の中が加速度的に進化する中で、**情報収集をするのが大変！** という事態になっています。
**Azure OpenAI**や **Language**、**Translator**とか使えば、それなりに良いツールができるのでは？
という発想から、[Azure Functions](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-overview?pivots=programming-language-csharp)を使った**記事収集ツールを作成しましたので、シェアします。**

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

# システム構成

![Qiita0505.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/779a148a-3467-645c-48e2-0cd50c1ecb49.jpeg)

1. **Power Automateを使い、RSS Feedの発行からトリガー、Azure Functionsを実行**
1. **RSS Feedの`プライマリーリンク`をもとに、`BeautifulSoup`で文章の取得**
1. **Azure OpenAIで記事の要約**
1. **LINEで自分あてに投稿**

といった流れになります。
[Azure Functions](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-overview?pivots=programming-language-python)の選定理由は、Pythonをかじっていて、シンプルに好きだからです🐍

作成に着手した段階では、[Azure AI Service](https://learn.microsoft.com/ja-jp/azure/ai-services/what-are-ai-services)の
- [Language](https://learn.microsoft.com/ja-jp/azure/ai-services/language-service/)
- [Translator](https://learn.microsoft.com/ja-jp/azure/ai-services/translator/text-translation-overview)

上記も利用していましたが、[ある記事に出会い](https://zenn.dev/mesummery/articles/27b993c83d056b)、**Azure OpenAIで全て完結する**と知ったので、最終的にこの構成に落ち着きました。

https://zenn.dev/mesummery/articles/27b993c83d056b
またコーディングには、かなりGPTの力を借りました。

https://learn.microsoft.com/ja-jp/azure/ai-services/what-are-ai-services

https://learn.microsoft.com/ja-jp/azure/ai-services/language-service/summarization/how-to/document-summarization#try-document-extractive-summarization

https://learn.microsoft.com/ja-jp/azure/ai-services/translator/text-translation-overview

[Azure OpenAI](https://learn.microsoft.com/ja-jp/azure/ai-services/openai/)は

|項目|設定
|---|---
|モデル名|gpt-35-turbo-16k
|モデル バージョン|0613
|デプロイの種類|Standard


**上記でデプロイする割安プランにしています。**

https://learn.microsoft.com/ja-jp/azure/ai-services/openai/

<details><summary>当初の設計</summary>

1. Power Automateを使い、RSS Feedの発行からトリガー、Azure Functionsを実行
1. RSS Feedの`プライマリーリンク`をもとに、`BeautifulSoup`で文章の取得
（**ここまでは一緒**）
1. **Languageを使い、テキスト要約**
1. **Translatorを使い、翻訳**
1. LINEで自分あてに投稿

OpenAIのプロンプトに身を委ねる方法より、用途が明確化されていて、
かつAI Serviceのパワーも使われているサービスの使い勝手の良さを感じたことから、
当初はこの仕様にしました。

最終的に、OpenAI一本で良い感じにイケたので、却下しましたが、
[GitHubにはコード](https://github.com/DEmodoriGatsuO/AzAITextSummarize/blob/master/function_app_archive.py)を残しているので、よろしければ見てみてください。


https://github.com/DEmodoriGatsuO/AzAITextSummarize/blob/master/function_app_archive.py

</details>

# Power Automate
Power Automateの仕組みは、かなりシンプルです。
1. **[RSSコネクタ - フィード アイテムが公開されたとき](https://powerautomate.microsoft.com/ja-jp/connectors/details/shared_rss/rss/)でトリガーを設定**
1. **HTTP送信でAzure Functionsにポスト**

以上。あくまでトリガーするため。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2734332/3fe10377-f380-cf5a-fce2-42bf68a3c3f4.png)

`Azure Functions`の`エンドポイント`は、**ソリューションでシークレットの環境変数を用いることにより保護します。**
**Azure Key Vault に格納されているシークレットを参照**できるセキュアな方法です。

https://learn.microsoft.com/ja-jp/power-apps/maker/data-platform/environmentvariables-azure-key-vault-secrets

:::note warn
プレミアムコネクタの領域になります。
場合によっては、[ロジック アプリ](https://learn.microsoft.com/ja-jp/azure/logic-apps/logic-apps-overview)の起用で対応してください。
:::

https://powerautomate.microsoft.com/ja-jp/connectors/details/shared_rss/rss/

https://learn.microsoft.com/ja-jp/azure/logic-apps/logic-apps-overview

# Azure Functionsの全体
- Python - 3.11
- OS - Linux

[Azure Functionsの設定、デプロイはVisual Studio Codeの拡張機能を使いました](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-develop-vs-code?tabs=node-v4%2Cpython-v2%2Cisolated-process&pivots=programming-language-python)

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-develop-vs-code?tabs=node-v4%2Cpython-v2%2Cisolated-process&pivots=programming-language-python


## コード全体

```python:function_app.py
import azure.functions as func
import logging
import os
import requests
from bs4 import BeautifulSoup
from openai import AzureOpenAI

# Initialize the Azure OpenAI client using environment variables
openai_client = AzureOpenAI(
    azure_endpoint=os.getenv('AZURE_OPENAI_ENDPOINT'),
    api_key=os.getenv('AZURE_OPENAI_KEY'),
    api_version="2024-02-15-preview"
)

# Initialize the Azure Function App with specific HTTP authentication level
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)

@app.route(route="AzLINETextSummarization", methods=['POST'])
def az_ai_text_summarize(req: func.HttpRequest) -> func.HttpResponse:
    """HTTP trigger function to summarize and translate text from a URL, and send it via LINE Messaging API."""
    logging.info('Processing the HTTP request in Python Azure Function.')

    try:
        # Extract the JSON data from the incoming request
        data = req.get_json()
        url_to_summarize = data.get('url')
        
        # Validate that a URL has been provided in the request body
        if not url_to_summarize:
            return func.HttpResponse("Please provide a URL in the request body.", status_code=400)
        
        # Extract text from the specified URL using BeautifulSoup
        extracted_text = extract_text_from_url(url_to_summarize)
        if not extracted_text:
            return func.HttpResponse("Failed to extract text from the provided URL.", status_code=500)

        # Summarize and translate the extracted text
        extract_summary = extract_summary_and_analysis(extracted_text)
        if not extract_summary:
            return func.HttpResponse("Failed to generate a summary.", status_code=500)

        # Get the LINE Messaging API access token and send the extracted text
        line_channel_token = os.getenv('LINE_CHANNEL_ACCESS_TOKEN')
        result = send_line_message(line_channel_token, url_to_summarize, extract_summary)

        return func.HttpResponse(str(result), status_code=200)
    
    except Exception as e:
        logging.error(f"An error occurred: {e}")
        return func.HttpResponse("An error occurred while processing your request.", status_code=500)

def extract_text_from_url(target_url):
    """Extracts text content from the given URL using BeautifulSoup."""
    try:
        # Send an HTTP GET request to the provided URL and verify status
        response = requests.get(target_url)
        response.raise_for_status()

        # Parse the HTML content using BeautifulSoup
        soup = BeautifulSoup(response.text, 'html.parser')
        paragraphs = soup.find_all('p')

        # Return all text found within paragraph tags, joined as a single string
        return ' '.join(paragraph.get_text() for paragraph in paragraphs)
    except Exception as error:
        logging.error(f"Failed to fetch or parse content from URL: {error}")
        return None

def extract_summary_and_analysis(input_text: str) -> str:
    """
    Extracts a summary and analysis from the provided text using Azure OpenAI services.

    Args:
        input_text (str): The text content to be summarized.

    Returns:
        str: A formatted string containing the summary and analysis results.
    """
    # Construct the prompt to extract "Title" and "New Features or Improvements" in Japanese
    summary_prompt = f"""
    下記の文章から「タイトル」「新機能や改善点」を抽出し、日本語で出力してください。
    ----------
    {input_text}
    """
    # Generate the summary using the specified OpenAI chat model
    summary_response = openai_client.chat.completions.create(
        model=os.getenv('AZURE_OPENAI_DEPLOY_NAME'),
        messages=[
            {"role": "system", "content": summary_prompt},
        ]
    )
    summary_result = summary_response.choices[0].message.content

    # Construct the prompt to extract "Explanation" and "Importance" in Japanese
    analysis_prompt = f"""
    下記の文章から「解説」「重要度」を抽出し、日本語で出力してください。
    ----------
    {input_text}
    """
    # Generate the analysis using the same OpenAI chat model deployment
    analysis_response = openai_client.chat.completions.create(
        model=os.getenv('AZURE_OPENAI_DEPLOY_NAME'),
        messages=[
            {"role": "system", "content": analysis_prompt},
        ]
    )
    analysis_result = analysis_response.choices[0].message.content

    # Combine the summary and analysis results into a single formatted response
    combined_results = f"{summary_result}\n{analysis_result}"
    return combined_results

def send_line_message(channel_access_token, url_to_summarize, message_text):
    """Sends a message to LINE using the LINE Messaging API."""
    # Retrieve LINE Messaging API URL and user ID from environment variables
    line_api_url = "https://api.line.me/v2/bot/message/push"
    line_user_id = os.getenv('LINE_ID')

    # Prepare the request headers and payload for the initial URL message
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {channel_access_token}"
    }
    payload_url = {
        "to": line_user_id,
        "messages": [{"type": "text", "text": url_to_summarize}]
    }

    # Send the URL message to the LINE API
    requests.post(line_api_url, headers=headers, json=payload_url)

    # Prepare the request payload for the translated summary message
    payload_summary = {
        "to": line_user_id,
        "messages": [{"type": "text", "text": message_text}]
    }

    # Send the summary message to the LINE API and return the response JSON
    response = requests.post(line_api_url, headers=headers, json=payload_summary)
    return response.json()

```

GPT様様によって構成しています。
また[記事](https://zenn.dev/mesummery/articles/27b993c83d056b)のプロンプトを活用させていただきました🙇

https://zenn.dev/mesummery/articles/27b993c83d056b

`エンドポイント`や`APIキー`は`Azure Functions`の`環境変数`に設定しています。
`LangChain`を使うことにより、指定したURLから文字を取得することを知りましたが、対象とするソースの文章から`BeautifulSoup`で事足りると感じ、こちらを採用しています。

具体的な関数の説明を書いていくと、下記のとおりです。

## BeautifulSoupで文字を取得する

```python
def extract_text_from_url(target_url):
    """Extracts text content from the given URL using BeautifulSoup."""
    try:
        # Send an HTTP GET request to the provided URL and verify status
        response = requests.get(target_url)
        response.raise_for_status()

        # Parse the HTML content using BeautifulSoup
        soup = BeautifulSoup(response.text, 'html.parser')
        paragraphs = soup.find_all('p')

        # Return all text found within paragraph tags, joined as a single string
        return ' '.join(paragraph.get_text() for paragraph in paragraphs)
    except Exception as error:
        logging.error(f"Failed to fetch or parse content from URL: {error}")
        return None
```

上記の部分です。`target_url`にフィードのURLを渡すことで、文字を取得・パースします。
軽くて、速くて、見やすくて好きです。

## Azure OpenAIで投稿用に文字を生成する

```python
def extract_summary_and_analysis(input_text: str) -> str:
    """
    Extracts a summary and analysis from the provided text using Azure OpenAI services.

    Args:
        input_text (str): The text content to be summarized.

    Returns:
        str: A formatted string containing the summary and analysis results.
    """
    # Construct the prompt to extract "Title" and "New Features or Improvements" in Japanese
    summary_prompt = f"""
    下記の文章から「タイトル」「新機能や改善点」を抽出し、日本語で出力してください。
    ----------
    {input_text}
    """
    # Generate the summary using the specified OpenAI chat model
    summary_response = openai_client.chat.completions.create(
        model=os.getenv('AZURE_OPENAI_DEPLOY_NAME'),
        messages=[
            {"role": "system", "content": summary_prompt},
        ]
    )
    summary_result = summary_response.choices[0].message.content

    # Construct the prompt to extract "Explanation" and "Importance" in Japanese
    analysis_prompt = f"""
    下記の文章から「解説」「重要度」を抽出し、日本語で出力してください。
    ----------
    {input_text}
    """
    # Generate the analysis using the same OpenAI chat model deployment
    analysis_response = openai_client.chat.completions.create(
        model=os.getenv('AZURE_OPENAI_DEPLOY_NAME'),
        messages=[
            {"role": "system", "content": analysis_prompt},
        ]
    )
    analysis_result = analysis_response.choices[0].message.content

    # Combine the summary and analysis results into a single formatted response
    combined_results = f"{summary_result}\n{analysis_result}"
    return combined_results

```
`input_text`を引数に、**OpenAIで翻訳・要約を良い感じに実施してくれます。**

`OpenAI API`ですが、ライブラリのバージョン(OpenAI 1.0以降)から、書き方が変わります。
[learnのクイックスタートをみると、バージョンによる書き方の違いがでていることがわかります。](https://learn.microsoft.com/ja-jp/azure/ai-services/openai/quickstart?pivots=programming-language-python&tabs=command-line%2Cpython-new)

https://learn.microsoft.com/ja-jp/azure/ai-services/openai/quickstart?pivots=programming-language-python&tabs=command-line%2Cpython-new

## LINEで投稿する
LINEの投稿は[Messaging API](https://developers.line.biz/ja/reference/messaging-api/#send-push-message)にHTTP要求を送信します。

```python
def send_line_message(channel_access_token, url_to_summarize, message_text):
    """Sends a message to LINE using the LINE Messaging API."""
    # Retrieve LINE Messaging API URL and user ID from environment variables
    line_api_url = "https://api.line.me/v2/bot/message/push"
    line_user_id = os.getenv('LINE_ID')

    # Prepare the request headers and payload for the initial URL message
    headers = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {channel_access_token}"
    }
    payload_url = {
        "to": line_user_id,
        "messages": [{"type": "text", "text": url_to_summarize}]
    }

    # Send the URL message to the LINE API
    requests.post(line_api_url, headers=headers, json=payload_url)

    # Prepare the request payload for the translated summary message
    payload_summary = {
        "to": line_user_id,
        "messages": [{"type": "text", "text": message_text}]
    }

    # Send the summary message to the LINE API and return the response JSON
    response = requests.post(line_api_url, headers=headers, json=payload_summary)
    return response.json()

```

https://developers.line.biz/ja/reference/messaging-api/#send-push-message

[具体的な始め方は、公式リファレンスを参考にしてください。](https://developers.line.biz/ja/docs/messaging-api/getting-started/)

https://developers.line.biz/ja/docs/messaging-api/getting-started/

# おわりに
**プロンプトの書き方次第で、翻訳からまとめまでガッツリできることに驚きました。**

本当に参考になりました。ありがとうございました！

https://zenn.dev/mesummery/articles/27b993c83d056b

**Power Automateの底力も、Azure Functionsによって底上げされるので、Pythonもガリガリ学んでいきたいですね！**
コードの問題点などは、[GitHub](https://github.com/DEmodoriGatsuO/AzAITextSummarize/tree/master)やコメントからお寄せください。

https://github.com/DEmodoriGatsuO/AzAITextSummarize/tree/master