🚀

AWS Strands AgentとUpstage APIを使った名刺画像から社員URLを取得する

に公開

はじめに

Fusicのレオナです。今回は、UpstageのUniversal Information Extraction APIを活用し、弊社の名刺画像から名前を抽出します。そして、その名前をもとにメンバー紹介ページから該当URLを取得する、AWS Strands Agentを用いたエージェントを作成してみました。
エージェントは抽出した名前をもとに、指定した会社のメンバー紹介ページをWebスクレイピングし、該当する社員のプロフィールページURLを取得するカスタムツールを実装しました。

なお、弊社は生成AI分野における包括的な協業を目指し、Upstageと業務提携を結んでいます。詳細はこちらからご覧ください。

Universal Information Extraction とは

Universal Information Extraction APIは、申請書や契約書、報告書などのさまざまな形式の書類から、必要な情報を取り出して整理する機能です。特別な学習や設定をしなくても、すぐに使えるのが特徴です。

このAPIは、どんな情報を取り出したいかを「スキーマ」として指定できます。本ブログでは、手動でスキーマを作る方法である Manual Schema Design を用いました。

  • こちらの記事も合わせてご覧ください。

https://zenn.dev/fusic/articles/840af2c906f479

Strands Agentとは

Strands AgentsはAWSが2025年5月に公開したオープンソースのAIエージェント構築SDKです。数行のPythonコードでAIエージェントを作成できる、モデル駆動型のフレームワークです。Strands AgentはLLMの推論力で計画、CoT(Chain-of-Thought)、ツールの呼び出しを行えます。Amazon Bedrock、AnthropicなどのLLMプロバイダーや、LiteLLM経由で利用できるその他のプロバイダーを含め、推論とツール使用機能を持つモデルもサポートしています。ツールとは、エージェントが外部システムと連携して、データの取得や環境の操作など、単なるテキスト生成だけでは実現できない処理を行うための機能モジュールを指します。

  • こちらの記事も合わせてご覧ください。

https://zenn.dev/fusic/articles/8dd670c37a8d68

やったこと


本ブログでは、実際に使用した名刺の画像は掲載しませんが、処理の流れは以下の通りです。

  1. 撮影した名刺の画像データから、Universal Information Extraction APIを使用して名前を抽出します。
  2. 抽出した名前を用いて、Strands Agentが弊社のメンバー紹介ページから該当する名前の社員プロフィールページのURLを取得します。

準備:エージェントのモデル有効化

  • 今回はClaude Sonnet 3.7を使用します。もしモデルの有効化ができていない場合は、以下の手順でモデルの有効化を行ってください。
    • マネジメントコンソールから、Amazon Bedrock を開く -> 左ナビゲーションからモデルアクセスをクリック
    • モデルアクセスを変更ボタンをクリック
    • Claude Sonnet 3.7を選択して次へボタンをクリック後、画面が変わるので送信ボタンをクリックします。
  • AWS CLIの認証情報を設定し、環境変数として保存します。

準備:UpstageのAPIを取得する

  1. こちらからアカウントを作成する
  2. DashboardのAPI keysからAPIキーを作成・取得する
  • 使用するモデル :

    • information-extract
  • 今回はUpstageのモデルを使用しますが、OpenAIと互換性があるため、OpenAIのライブラリを使用しています。

  • ディレクトリ構造
    プロジェクト全体を以下のようなディレクトリ構造で作成しました。

my_project/
├── ie.py            # 情報抽出するスクリプト
├── main.py
├── run_agent.py     # エージェントがタスクを実行する
├── test.jpg         # (処理対象の名刺画像)
├── .env
└── tools/
    ├── __init__.py
    └── search_member.py
  • ライブラリインストール
uv pip install openai requests beautifulsoup4 strands-agents strands-agents-tools strands-agents-builder

実装

ie.py

まず、名刺画像から名前を抽出するスクリプト ie.pyを作成します。

import base64
import json
from openai import OpenAI

client = OpenAI(
    api_key="UPSTAGE_API_KEY",
    base_url="https://api.upstage.ai/v1/information-extraction"
)

def encode_img_to_base64(img_path):
    with open(img_path, 'rb') as img_file:
        img_bytes = img_file.read()
        base64_data = base64.b64encode(img_bytes).decode('utf-8')
        return base64_data

def extract_person_name(img_path: str) -> str | None:
    base64_data = encode_img_to_base64(img_path)
    
    # try-except ブロックを削除
    extraction_response = client.chat.completions.create(
        model="information-extract",
        messages=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "image_url",
                        "image_url": {"url": f"data:image/png;base64,{base64_data}"}
                    }
                ]
            }
        ],
        response_format={
            "type": "json_schema",
            "json_schema": {
                "name": "document_schema",
                "schema": {
                    "type": "object",
                    "properties": {
                        "company_name": {
                            "type": "string",
                            "description": "The name of bank in bank statement"
                        },
                        "person_name": {
                            "type": "string",
                            "description": "The name of the person extracted from the document"
                        }
                    },
                    "required": ["person_name"]
                }
            }
        }
    )
    response_content = extraction_response.choices[0].message.content
    if response_content:
        data = json.loads(response_content)
        return data.get("person_name")
    return None # response_content がない場合はNoneを返す

tools/search_member.py

次に、抽出した名前を使って、メンバー紹介ページから該当する社員のプロフィールページURLを検索するスクリプトtools/search_member.pyを作成します。これはAgentが行うタスクです。

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin

def search_member_by_name(name):
    base_url = "https://fusic.co.jp/members"

    resp = requests.get(base_url, timeout=5)

    soup = BeautifulSoup(resp.text, "html.parser")

    # 全てのメンバーリストアイテムを取得
    member_list_items = soup.select("div.c-member__list ul li")

    found_url = None
    for item in member_list_items:
        # 各アイテムから名前とリンクを取得
        name_tag = item.find("p", attrs={"data-item": "title"})
        link_tag = item.find("a")

        if name_tag and link_tag:
            member_name_from_html = name_tag.get_text(strip=True)
            
            # 検索名とHTML内の名前から空白を除去して比較
            normalized_query_name = "".join(name.split()) # 半角・全角スペースを除去
            normalized_html_name = "".join(member_name_from_html.split())

            if normalized_query_name in normalized_html_name:
                href = link_tag.get("href")
                if href:
                    found_url = urljoin(base_url, href)
                    break # 最初に見つかったメンバーで終了

    if found_url:
        return found_url
    else:
        return f"{name} さんのプロフィールが見つかりませんでした。"

run_agent.py

名刺から名前を抽出したら、次はその名前を使ってメンバー情報を検索するエージェントを実行します。run_agent.py を作成し、tools/search_member.py で定義した検索機能をツールとして組み込みます。

import logging
import io 
import sys 
from strands import Agent, tool

# カスタムツールとして search_member_by_name を取り込む
from tools.search_member import search_member_by_name

@tool(
    name="search_member", 
    description="与えられたメンバー名を https://fusic.co.jp/members から検索してプロフィール URL を返します。"
)
def search_member(name):
    """
    strands Agent用のツール関数。内部で search_member_by_name を呼び出す。
    """
    return search_member_by_name(name)

def run_fusic_agent(person_name):
    """
    指定された人物名でFusicのメンバー検索エージェントを実行し、結果を出力する。
    Agentからの不要な標準出力はキャプチャして抑制する。
    """
    # ログレベルをCRITICALに設定
    logging.basicConfig(level=logging.CRITICAL)

    agent = Agent(tools=[search_member])

    prompt = f"「{person_name}」さんのプロフィールURLを検索してください。出力はURLのみです。"

    # 標準出力を一時的にキャプチャする準備
    # Agentの出力のみ表示する
    old_stdout = sys.stdout
    sys.stdout = captured_output = io.StringIO()

    # Agentを実行
    response = agent(prompt)

    # 標準出力を元に戻す
    sys.stdout = old_stdout

    # 指定されたフォーマットで結果を表示
    if response:
        print(f"{person_name}さんのプロフィールURLは以下になります:\n{response}")
    else:
        print(f"{person_name}さんのプロフィールURLは見つかりませんでした。")


main.py

これまで作成した情報抽出スクリプト (ie.py) とエージェント実行スクリプト (run_agent.py) を順に呼び出し、一連の処理を実行するための起点となるファイルです。

import argparse
from ie import extract_person_name
from run_agent import run_fusic_agent

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("image_path", type=str)
    args = parser.parse_args()
    # 画像から名前を抽出
    person_name = extract_person_name(args.image_path)

    if person_name:
        run_fusic_agent(person_name)
    else:
        print("読み取った画像から名前を抽出できなかったため、メンバーを検索できませんでした。")

if __name__ == "__main__":
    main()

結果

以下のコマンドでスクリプトを実行できます。

python main.py test.jpg
佐 藤 礼 央 奈さんのプロフィールURLは以下になります:
https://fusic.co.jp/members/184

最後に

ブログでは実際に撮影した写真は掲載していませんが、Agentのタスクとしてメンバー検索を実行する機能を実装し、名刺の画像から弊社メンバーのプロフィールページURLを取得することができました。

Fusic 技術ブログ

Discussion