ENECHANGE Developer Blog

ENECHANGE開発者ブログ

SlackbotにFunction callingによる画像生成機能を追加した

CTO室の岩本 (iwamot) です。

弊社SlackのChatGPT botに画像生成機能を追加しました。Function callingを使って、OpenAIの画像生成エンドポイントを呼び出すシンプルなものです。

この記事では、実装の詳細と、機能追加した背景をご紹介します。

実装の詳細

下記が、画像生成機能のソースコード (app/function_call.py) です。42行しかありません。

import json
import openai
import os


def create_image(prompt, size="256x256"):
    """Creates an image given a prompt"""
    openai.api_key = os.environ["FUNCTION_CALL_OPENAI_API_KEY"]
    openai.api_type = "open_ai"
    openai.api_base = "https://api.openai.com/v1"
    openai.api_version = None
    response = openai.Image.create(prompt=prompt, size=size)
    image_info = {
        "prompt": prompt,
        "size": size,
        "image_url": response["data"][0]["url"],
        "query_strings_deletable": "false",
    }
    return json.dumps(image_info)


functions = [
    {
        "name": "create_image",
        "description": "Creates an image given a prompt",
        "parameters": {
            "type": "object",
            "properties": {
                "prompt": {
                    "type": "string",
                    "description": "A text description in English of the desired image",
                },
                "size": {
                    "type": "string",
                    "description": "The size of the generated images",
                    "enum": ["256x256", "512x512", "1024x1024"],
                },
            },
            "required": ["prompt"],
        },
    },
]

ChatGPT botで使っているChatGPT in Slackが、先日Function callingをサポートしたため、これだけで実現できました。

以下、細かな補足です。

  • メインのチャット機能ではAzure OpenAI Service (Japan East) を使っていますが、画像生成機能ではOpenAI APIを呼び出しています。Azureだと、US Eastでしか画像生成モデルのDALL·Eがまだ使えないためです
  • 関数の戻り値に "query_strings_deletable": "false" を含めているのは、ChatGPT botに「画像URLのクエリストリングを削除してはいけないよ」と伝える意図です。実際、不完全なURLが表示されるケースが減りました

機能追加した背景

画像生成機能を追加したいちばんの目的は、Function callingの存在や可能性を同僚に知ってもらうことでした。Function callingをうまく使えば、ChatGPT botの能力が拡がり、いっそうの業務効率化につながります。

追加する機能自体は何でもよかったのですが、全職種で使えることや、インパクトを重視した結果、画像生成が浮かんだ次第です。

ちなみに、他のアイディアには下記がありました。

  • 提供しているシステムのサービスレベル(SLOや実測値)を取得する
  • GitHub IDとスタッフ名を紐づけて検索できるようにする
  • 入力されたURLに対応するQRコードを生成する
  • 入力されたIPv4アドレスに関する情報を表示する

今後

前掲のソースコードは素朴すぎるので、もし画像生成機能が継続的に使われるようなら、下記のように改善したいと思っています。

  • 画像をS3に置いて永続化する(今はURLが1時間で無効になる)
  • プロンプトが日本語だと思ったような画像にならない場合が多いため、英語に訳してからエンドポイントを呼び出す

また、他の機能の追加も目指したいところです。どんな機能があれば助かるか、引き続き考えてみます。

追記 (2023-08-31)

とりあえず永続化を進めました。URLが短くなったので、不完全なURLが表示される問題も解決したはずです。

また、QRコード生成機能・音声読み上げ機能も追加しました。

import boto3
import json
import openai
import os
import qrcode
import urllib3
import uuid
import tempfile


def _upload_to_s3(file, key, content_type):
    s3 = boto3.client("s3")
    if isinstance(file, str):  # Assuming it's a file path
        s3.upload_file(
            file,
            os.environ["FUNCTION_CALL_WEB_BUCKET"],
            key,
            ExtraArgs={"ContentType": content_type},
        )
    else:  # Assuming it's a file-like object
        s3.upload_fileobj(
            file,
            os.environ["FUNCTION_CALL_WEB_BUCKET"],
            key,
            ExtraArgs={"ContentType": content_type},
        )


def _generate_object_key(dir, extension):
    return "{}/{}.{}".format(dir, uuid.uuid4(), extension)


def _build_url_for_object(key):
    return f"https://{os.environ['FUNCTION_CALL_WEB_DOMAIN']}/{key}"


def create_image(prompt, size="256x256"):
    """Creates an image given a prompt"""
    openai.api_key = os.environ["FUNCTION_CALL_OPENAI_API_KEY"]
    openai.api_type = "open_ai"
    openai.api_base = "https://api.openai.com/v1"
    openai.api_version = None
    image = openai.Image.create(prompt=prompt, size=size)
    http = urllib3.PoolManager()
    r = http.request("GET", image["data"][0]["url"], preload_content=False)
    key = _generate_object_key("images", "png")
    _upload_to_s3(r, key, "image/png")
    r.release_conn()
    image_info = {
        "prompt": prompt,
        "size": size,
        "image_url": _build_url_for_object(key),
    }
    return json.dumps(image_info)


def create_qrcode(url):
    """Creates a QR code given a URL"""
    img = qrcode.make(url)
    key = _generate_object_key("images", "png")
    with tempfile.NamedTemporaryFile() as tf:
        img.save(tf.name)
        _upload_to_s3(tf.name, key, "image/png")
    qrcode_info = {
        "url": url,
        "qrcode_image_url": _build_url_for_object(key),
    }
    return json.dumps(qrcode_info)


def synthesize_speech(text, gender="female"):
    """Synthesizes speech given text"""
    voice_id = {"female": "Tomoko", "male": "Takumi"}[gender]
    response = boto3.client("polly").synthesize_speech(
        VoiceId=voice_id,
        OutputFormat="mp3",
        Text=text,
        Engine="neural",
    )
    key = _generate_object_key("audios", "mp3")
    _upload_to_s3(response["AudioStream"], key, "audio/mpeg")
    speech_info = {
        "text": text,
        "gender": gender,
        "voice_id": voice_id,
        "audio_url": _build_url_for_object(key),
    }
    return json.dumps(speech_info)


functions = [
    {
        "name": "create_image",
        "description": "Creates an image given a prompt",
        "parameters": {
            "type": "object",
            "properties": {
                "prompt": {
                    "type": "string",
                    "description": "A text description in English of the desired image",
                },
                "size": {
                    "type": "string",
                    "description": "The size of the generated images",
                    "enum": ["256x256", "512x512", "1024x1024"],
                },
            },
            "required": ["prompt"],
        },
    },
    {
        "name": "create_qrcode",
        "description": "Creates a QR code given a URL",
        "parameters": {
            "type": "object",
            "properties": {
                "url": {
                    "type": "string",
                    "description": "The URL to encode in the QR code",
                },
            },
            "required": ["url"],
        },
    },
    {
        "name": "synthesize_speech",
        "description": "Synthesizes speech given text",
        "parameters": {
            "type": "object",
            "properties": {
                "text": {
                    "type": "string",
                    "description": "The text to synthesize",
                },
                "gender": {
                    "type": "string",
                    "description": "The gender of the voice",
                    "enum": ["female", "male"],
                },
            },
            "required": ["text"],
        },
    },
]