🐒

プレゼントもらえるかどうかクリスマス前に知りたい!!!そう思った幼少期〜Function Callingの活用〜

2023/12/24に公開

はじめに

この記事は、ラクスパートナーズ Advent Calendar 2023 25日目最終日の記事です
弊社はITに関わるすべての人たちを応援する楽楽パートナーを掲げ、
日々お客様の業務の支援を行なっております。

TL;DR

余計な話とかいらないから技術的に何しているか教えろ!!という人向けに
本記事は大きくトピック分けすると

  1. OPENAIのFunctionCalling機能
  2. Pineconeを活用した類似文章検索
  3. FastAPIとstreamlitを活用したWebアプリケーション作成

この三つのうち1,2に関する内容となっています。
3については含めると量がすごいことになるので、年明け目安に別記事で上げたいと思います。(気になる方は見ていただけると幸いです)

1.概要

いよいよ本記事について書いていきます。
皆さん、クリスマスといえば何を思い浮かべますか?
豪華なディナー、煌びやかな街並み、非リアの嫉妬と悲しみで荒れ狂ったX(旧Twitter) etc...
まあ、色々あると思いますが今回クリスマスに記事を投稿ということでクリスマス関連で初めに思いついたのがクリスマスプレゼントでした。
(我が家では25日の朝に玄関にあるクリスマスツリーの下のプレゼントを確認しにダッシュするのが恒例行事でした🤣)

して、ここから今回作ったものに関係する内容になるのですが皆さん幼少期にご両親から

「良い子にしてたらサンタさんからプレゼントをもらえるよ」

と言われたことはないでしょうか?
我が家では毎年のように12月に入ると言われていました、、、
プレゼントは絶対もらえるとわかっていても当時の私はチビながら自分が頑張ってもサンタさんが悪い子認定したらプレゼントをもらえないのは気に食わん!!ってよく思っていました。

前座が長くなりましたね
ってことで今回の記事で話していくのは

「良い子かどうか定量的に判断してくれるアプリケーション」

についてになっています。

2.アプリケーションの概要

アーキテクチャ図

今回のアプリケーションは以下のような構成になっています。

ざっくり概要図

ディレクトリ構成

.
├── app.py                             Streamlitでフロントエンド実装
└── api
	├── __init__.py
	├── main.py                       FastAPIのバックエンド実装
	├── config
		  └──openai_config.py     Pydanticによる型定義
	├── functions
		  ├──decide_kids.py    CallingFunction機能で呼び出す関数
		  ├──inner_action.py    Callingfucntion機能の実装
		  └──get_result.py     functions内部の動作をまとめたもの

実際の操作画面と結果

  • 今回は時間の都合上、至ってシンプルな作りになっております
    (本当は内装もクリスマス仕様とかにしたかった、、、。)

良い子判定の時

操作画面-結果が良い子の時-

悪い子判定の時

操作画面-結果が良い子の時-

3.良い子か判断+Function Callingの活用(バックエンド部分)

良い子か判断する部分(decide_kids.py)

ここでは、ベクトルDBのひとつであるPineconeを活用した類似文章検索を利用しています。
Pinecondeについては以下の記事が概要としてはわかりやすいのでぜひPinecone気になった方は読んでみてください
https://note.com/pharmax/n/n28b23641f3be

そもそも類似文章検索って?

そこまでガッツリ話すわけではないですがざっくり今回の中身で何しているかお伝えします。
ざっくり言うと、手元にあるテキストデータをベクトル化してPineconeに保存。入力となる文章をベクトル化した時に保存したベクトル情報と近しいものをPinconeから引っ張ってくると言うものになります。

詳細手順(コード付き)
  1. 手元で良い行動、悪い行動とその行動のスコア(-10~+10で-に大きいほど悪,+に大きいほど良)をペアにしてcsvファイルもしくはjsonファイルを作成しておく
    作成したcsvファイルの一部(スコアに関しては主観です)
  2. 1で作成したファイルのうち文章部分をLLMなどの自然言語モデルでベクトル化する
#言語モデルのインスタンス化
embeddings = OpenAIEmbeddings() #今回はOPENAIフル活用です
#テキストのベクトル化
text_vec = embeddings.embed_query(text) #これは一つの文章に対してなのでapplyやfor文を活用
  1. 2で作成したベクトル,テキスト,そのテキストに紐づいた行動スコア,行動の善悪の4つとその識別idをまとめたものを作成する
upserts=[]
for i in range(len(vec_list)): #送るテキストの数だけloop
  upserts.append(
      {
          'id':str(i),#識別用id
          'values':vec_list[i],#ベクトル情報
          'metadata':{
			  'action_text':text_list[i], #文章
			  'score':int(score_list[i]), #行動スコア
			  'filter':filter_list[i]   #行動の善悪
			}
      }
  )
  1. Pineconeのindexを作成し、作成したindexにupsertしてデータを保存する
#indexの作成
pinecone.create_index(
    'advent-carender',#名前
    dimension=1536,#ベクトルの次元数
    metric='cosine',#検索手法
    pod_type='s1'#DBの種類の指定
)
#indexへ接続
index = pinecone.Index('advent-carender')
#pineconeに投入
index.upsert(upserts)
  1. 入力文章をベクトル化する(この時のモデルは2で使用したモデルと同じものを用いる)
#Input
good_ac = '友達をたくさん助けた'
#ベクトル化
embeddings = OpenAIEmbeddings()
good_vec = embeddings.embed_query(good_ac)
  1. pineconeのqueryメソッドを活用して似たベクトル情報を持つテキストを抽出する
    (この時類似度を測るものにはcos類似度を使用しています。)
#検索の実施(good_ac)
result_good = index.query(
    top_k = 3,#表示件数
    vector=good_vec,#ベクトル情報
    include_metadata=True,#メタデータの取り扱い
    filter = {'filter':'善'}#行動の善悪でフィルターして検索
)
  • 詳細手順の6の項目のコード部分に含んでいますが今回Pineconeを活用した理由の一つとしてmetadataでフィルタリングできるという点があります(高速クエリ検索とかも理由としてありますが、、)

今回作成したdecide_kids.pyについて

下に収納したのがdecide_kids.pyの全体になります

decide_kids.py
from api.config.openai_config import OPENAI_config
import openai
from openai import Embedding
import pinecone

#openaiのAPIKEYの登録
openai.api_key = 自分のAPIkey

#pineconeのindex情報の取得
def init_pinecone():
    #pineconeのapikey
    PINECONE_APIKEY= 自分のAPIkey
    pinecone.init(
    api_key=PINECONE_APIKEY,
    environment='us-west4-gcp-free'
    )
    #pineconeの接続
    index = pinecone.Index('advent-carender')
    return index

def decide_kids(action_text:str, ac_type:str, top_k:int=3)->float:
    '''
    [概要]
    入力テキストとtypewpもとにpinecone内のデータを参照して行動に対する良い子scoreを算出する関数
    [Input]
    * action_text: 行動テキスト
    * ac_type:善 or 悪
    * top_k:参照するデータ数を指定する(デフォルトは3)
    [Output]
    * ac_score:行動に対するscore
    '''
    index = init_pinecone()
    #テキストのベクトル化
    text_vec = Embedding.create(
        model = 使用する自然言語モデルの指定,
    	input=[action_text]
    )
    #検索の実施(good_ac)
    result = index.query(
        top_k = top_k,
        vector=text_vec['data'][0]['embedding'],
        include_metadata=True,
        filter = {'filter':ac_type}
    )

    #行動スコアの算出(上位top_k個の平均)
    sum_score = 0
    for j in range(top_k):
        tmp = result['matches'][j]['metadata']['score']
        sum_score+=tmp
    ac_score = sum_score/top_k
        
    return ac_score

内容についてはほぼ詳細手順で使ったものを流用したので省略します。
最後の行動スコア算出については安直ですが、抽出したtop_k個の単純平均で算出しています。
(行動スコアの算出も自分でチューニングしたモデルを使いたかった、、、、!!!!時間がなさすぎましたね)

Function Calling機能について(inner_action.py)

次は今回のメインであるOPENAIがFunction Calling機能について話していきます。
https://platform.openai.com/docs/guides/function-calling

inner_action.py
from api.functions.decide_kids import decide_kids
from api.config.openai_config import OPENAI_config
import openai
from openai import ChatCompletion
import json

#openaiのAPIKEYの登録
openai.api_key = OPENAI_config.OPENAI_APIKEY





class Inner_action:
    def __init__(self):
        self.model = "gpt-3.5-turbo-0613"
        
    def first_action(self):
        '''
        【概要】
        opeanaiのFunctionCalling機能を利用して関数呼び出しの可否を決める
        【Output】
        * response:関数利用するかどうかの判断
        '''

        #ユーザー側からの入力と関数について定義
        content = f'この人がした行動内容は{self.text}ですこの行動の善悪を決めたいです'
        user_message = {'role':'user', 'content':content}
        messages = self.init_message+[user_message]
        self.messages = messages
        #関数内容の定義
        functions = [
            {
                "name": "decide_kids",
                "description": "文章を出力する",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "action_text": {
                            "type": "string",
                            "description": "日本語表記で行動内容が明記されている",
                        },
                        "ac_type": {
                            "type": "string",
                            "description": "日本語表記で行動の善悪が明記されている",
                        },
                    },
                    "required": ["action_text", 'ac_type'],
                },
            }
        ]
        #上二つの定義をもとにGCPモデルが関数利用の可否を判断する
        response = ChatCompletion.create(
            model=self.model,
            messages = messages,
            functions=functions,
            function_call='auto' #関数を使用するか否かの判断もしてくれる
        )
        self.response = response

    def second_action(self,text:str, init_message:list[str]=[]):
        '''
        【概要】
        first_actionの出力から関数の実行を行い、行動スコアをメッセージと共に返す関数
        【Input】
	* text: 行動テキスト
        * init_message:初期メッセージ(デフォルトは空リスト)
        * message:first_actionでGPTモデルに入力した内容
        * response:first_actionで得たGPTモデルの判断結果
        【Output】
        * message:関数の実行の可否と実行した場合の結果含むメッセージ内容
        '''
        #入力のインスタンス化
        self.text = text
        self.init_message = init_message
        
        #first_actionの実施
        self.first_action()
        
        #responseから判断結果の取得
        response_message = json.dumps(self.response['choices'][0]['message'].to_dict_recursive(), indent=2, ensure_ascii=False).replace(r'\n', '').replace(r'\\n', '').replace(r'\\', '').replace('  ', '')
        response_message = json.loads(response_message)
        
        #関数を実行するか否かで条件分岐
        if response_message['function_call']:
            #実行関数に関連する情報を取得
            function_name=response_message['function_call']['name'] #実行関数名
            function_args=json.loads(response_message['function_call']['arguments']) #実行関数に入力する変数
            
            #実行する関数が時程したものか否かで証券分岐
            if function_name == 'decide_kids':
                #入力情報の取得
                action_text = function_args['action_text']
                ac_type = function_args['ac_type']
                #decide_kidsの実施
                function_response=decide_kids(action_text=action_text,ac_type=ac_type)
        
            else:
                finction_response = 'Not score'
                
        return function_response

Function Calling機能

みなさんOPENAIのAPIにあるFunction Callingをご存知でしょうか?
私も知ってはいたものの今回初めて活用しました。
簡単に言うとfunctionsという名前の辞書型で関数やその入力に必要なものなどをLLMに与えてあげるとLLMが関数を実行するか否かを判断してくれるというものになります(複数関数がある場合はその中から適切なものを選択)
また、その関数に必要な入力を人間のテキストから必要なものだけ抜き出してくれるというものになってます。
今回で注目して欲しいのがac_type(行動に対して善悪のいずれか)を入力文章には含んでいないという点になります。

content = f'この人がした行動内容は{self.text}ですこの行動の善悪を決めたいです'
user_message = {'role':'user', 'content':content}
messages = self.init_message+[user_message]
self.messages = messages
#関数内容の定義
functions = [
    {
	"name": "decide_kids",
	"description": "文章を出力する",
	"parameters": {
	    "type": "object",
	    "properties": {
		"action_text": {
		    "type": "string",
		    "description": "日本語表記で行動内容が明記されている",
		},
		"ac_type": {
		    "type": "string",
		    "description": "日本語表記でaction_textの内容をもとにした行動の善悪が明記されている",
		},
	    },
	    "required": ["action_text", "ac_type"],
	},
    }
]

functionsには関数名とそのパラメータを指定するのですが今回で言うと最後の'"required": ["action_text", "ac_type"]'の部分が必要なパラメータとなっています。
それに対してOPENAIのLLMに対する入力は以下になります。

from openai import ChatCompletion #openai==0.28
#textは入力文章
content = f'この人がした行動内容は{text}ですこの行動の善悪を決めたいです'
user_message = {'role':'user', 'content':content}
#入力メッセージ
message = init_message + [user_message] #init_messageは空リスト
#Function Callingを含むLLMのレスポンス
response = ChatCompletion.create(
            model=self.model,
            messages = messages,
            functions=functions,
            function_call='auto' #関数を使用するか否かの判断もしてくれる
        )

content部分には、見てわかる通りtextに対する行動の善悪は明記されていません。
しかし、textの情報をもとにLLMがac_typeを推測して出力であるresponse内にac_typeに関する情報として返してくれます。

これが今回の良い子判断にどう役立つんだよ!!
って思った方いるかもしれません。
フロント部分を説明してないと難しいのですが、今回のUIが下のようになっていて良い行動と悪い行動を入力するフォームが分かれています。
今回作成したアプリケーションのUI

そして対象はちっちゃい子供が入力する前提で作っています。
なので仮に良い行動のフォームに悪い行動、反対に悪い行動のフォームに良い行動を入力してもpineconeでベクトル検索する時にフィルターが適切に作用するのに役立っています。

  • ただその判断が正しくされているかは微妙なのでプロンプトをもう少し工夫する必要はありそうですが、、、

最終的なバックエンド部分の内部

PineconeとFunction Callingそれぞれについて述べてきましたがそれらをまとめたのが以下のpyファイルになります

get_result.py
from api.functions.decide_kids import decide_kids
from api.functions.inner_action import Inner_action
from api.config.openai_config import OPENAI_config
import openai
from openai import ChatCompletion
import json

#openaiのAPIKEYの登録
openai.api_key = OPENAI_config.OPENAI_APIKEY
action = Inner_action()


def total_action(good_text:str, bad_text:str, init_message:list[str]=[])->dict[str]:
    '''
    全てののアクションを結合する
    ''' 
    #良い行動の評価を得る
    #アクション(善)
    good_response = action.second_action(good_text, init_message=init_message)
    #悪い行動の評価を得る
    #アクション(悪)
    bad_response = action.second_action(bad_text, init_message=init_message)
    
    #総合的な子供の評価を得る
    try:
        total_response = good_response+bad_response
        if total_response>=0:
            kids_type = '良い子'
            present_message = 'プレゼントをもらえるよ!'
        elif total_response<0:
            kids_type = '悪い子'
            present_message = '残念プレゼントはあげられないよ、、、来年は良い子にするんだよ'
    except Exception as e:
        print(e)
     
    return {
        "今年のあなた":f"{kids_type}",
        "プレゼント":f"{present_message}"
    }
def get_result(kids_result:dict, messages:str=[])->str:
    #関数の結果をメッセージを保存する
    function_result_message = {
        'role' : 'function',
        'name' : 'decide_kids',
        'content' : json.dumps(kids_result, ensure_ascii=False),
    }
    
    #メッセージの追加
    messages.append(function_result_message)
    #GPTのレスポンス取得
    final_response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo-0613",
        messages=messages,
    )
    result = final_response["choices"][0]["message"]["content"]
    return result
  • このpythonファイルは二つの関数で分けています
    • total_action
      inner_action.pyの内容を良い行動、悪い行動それぞれで実施して結果を返す
    • get_result
      total_actionで得た結果をLLMに入力し、いい感じの文章を返してくれる

total_actionとget_resultの二つに分けた理由としては、入力して「良い子でした」or 「悪い子ですね」って単純に返すのだと子供的に辛辣すぎるなって思っていい感じにしてくれるようにLLMを活用しました。

とまあこんな感じの関数をあとはFastAPIを使ってAPI化してフロントに渡しているっていうのが今回のアプリケーションになります。

4.おわりに

というわけで、今回の記事はベクトルデータベースのPineconeによる文章検索についてとFunction Calling機能について書かせていただきました。
自分の記事を読んで気になる!!!と思っていただけたら幸いです。
個人的にこの記事はここ最近、年末ということもあって自己学習に時間があまり割けずいまいちな記事になってしまったと思います。すみません、、、🙇‍♂️
ただ、久しぶりに自然言語の個人開発をやって今回作ったものを改良したい欲が出てきたので来年のクリスマスにはもっとリッチなものになったよ!!っていう記事を書きたいですね〜

後最後に、今回参照する良い行動リスト、悪い行動リストを作成するにあたって協力していただいたRPの方々とても助かりました。ありがとうございます。

5.参考資料

Discussion