3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

世の中が加速度的に進化する中で、情報収集をするのが大変! という事態になっています。
Azure OpenAILanguageTranslatorとか使えば、それなりに良いツールができるのでは?
という発想から、Azure Functionsを使った記事収集ツールを作成しましたので、シェアします。

システム構成

Qiita0505.jpg

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

といった流れになります。
Azure Functionsの選定理由は、Pythonをかじっていて、シンプルに好きだからです🐍

作成に着手した段階では、Azure AI Service

上記も利用していましたが、ある記事に出会いAzure OpenAIで全て完結すると知ったので、最終的にこの構成に落ち着きました。

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

Azure OpenAI

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

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

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

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

最終的に、OpenAI一本で良い感じにイケたので、却下しましたが、
GitHubにはコードを残しているので、よろしければ見てみてください。

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

Power Automate

Power Automateの仕組みは、かなりシンプルです。

  1. RSSコネクタ - フィード アイテムが公開されたときでトリガーを設定
  2. HTTP送信でAzure Functionsにポスト

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

image.png

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

プレミアムコネクタの領域になります。
場合によっては、ロジック アプリの起用で対応してください。

Azure Functionsの全体

  • Python - 3.11
  • OS - Linux

Azure Functionsの設定、デプロイはVisual Studio Codeの拡張機能を使いました

コード全体

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様様によって構成しています。
また記事のプロンプトを活用させていただきました🙇

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

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

BeautifulSoupで文字を取得する

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で投稿用に文字を生成する

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のクイックスタートをみると、バージョンによる書き方の違いがでていることがわかります。

LINEで投稿する

LINEの投稿はMessaging APIにHTTP要求を送信します。

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()

具体的な始め方は、公式リファレンスを参考にしてください。

おわりに

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

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

Power Automateの底力も、Azure Functionsによって底上げされるので、Pythonもガリガリ学んでいきたいですね!
コードの問題点などは、GitHubやコメントからお寄せください。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?