import os import time import datetime import gradio as gr from openai.types.beta.threads.runs import ToolCallsStepDetails from openai import ( OpenAI, AuthenticationError, NotFoundError, BadRequestError ) # GPT用設定 # SYS_PROMPT_DEFAULT = "あなたは優秀なアシスタントです。質問をされた場合は、質問に答えるコードを作成して実行します。回答は日本語でお願いします。" # アシスタント用設定 DF_MODEL = "gpt-3.5-turbo-1106" ASSIST_NAME = "Code Interpreter Assistant for O3" AST_SYS_PROMPT = "あなたは優秀なアシスタントです。質問をされた場合は、質問に答えるコードを作成して実行します。回答は日本語でお願いします。" # DUMMY = "********************" # file_format = {".txt", ".csv", ".pdf"} file_format = {".txt", ".csv", ".jpg", ".jpeg", ".png", ".xlsx", ".pdf", ".zip"} # 各種フォルダ FONT_FILE_PATH = "for_assistants/NotoSansJP-Bold.zip" # IMG_FOLDER = "images" # ANT_FOLDER = "annotations" # 各種メッセージ PLACEHOLDER = "これは東京都の年別人口データです、折れ線グラフでデータの可視化をお願いします… など" IMG_MSG = "(画像ファイルを追加しました。リセットボタンの上に表示されています。)" ANT_MSG = "(下部の[出力ファイル]にファイルを追加しました。)" # 各種設定値 MAX_TRIAL = int(os.environ["MAX_TRIAL"]) # メッセージ取得最大試行数 INTER_SEC = int(os.environ["INTER_SEC"]) # 試行間隔(秒) # サンプル用情報 # examples = ["sample_data/東京都年別人口.csv", "sample_data/世界の人口2023年.csv", "sample_data/練馬区年齢別人口.csv", "sample_data/桃太郎あらすじ.txt"] examples = ["sample_data/東京都男女別人口_y23.csv","sample_data/東京都年齢別人口_y23.csv"] # example_toid = {"東京都年別人口.csv" : "file-TMLJxwdTy1oaaoo2l712r1Os" # ,"世界の人口2023年.csv" : "file-rafHPSkmMciOjKovah0u5oug" # , "練馬区年齢別人口.csv" : "file-B5VqVSqgh8dB70ygHwt0LVIH" # , "桃太郎あらすじ.txt" : "file-ylcNO5LShDYgDk5wiuSiyTMA"} # ,"South Africa-2023.csv" : "file-6aarxpoX4TISJjqPH2Pi3QMV"} # コード出力用 code_mode = {'ON': True, 'OFF': False} # 各関数 def set_state(openai_key, sys_prompt, code_output, state): """ 設定タブの情報をセッションに保存する関数 """ state["openai_key"] = openai_key state["system_prompt"] = sys_prompt state["code_mode"] = code_mode[code_output] return state def init(state, text, file): """ 入力チェックを行う関数 ※ここで例外を起こすと入力できなくなるので次の関数でエラーにする """ err_msg = "" file_id = None if state["openai_key"] == "": # OpenAI API Key未入力 err_msg = "OpenAI API Keyを入力してください。(設定タブ)" return state, file_id, err_msg if not text: # テキスト未入力 err_msg = "テキストを入力して下さい。" return state, file_id, err_msg elif file: # 入力ファイルの形式チェック root, ext = os.path.splitext(file) if ext not in file_format: # ファイル形式チェック err_msg = "指定した形式のファイルをアップしてください。(注意事項タブに記載)" return state, file_id, err_msg try: if state["client"] is None: # 初回起動時は初期処理をする # os.environ["OPENAI_API_KEY"] = os.environ["TEST_OPENAI_KEY"] # テスト時 os.environ["OPENAI_API_KEY"] = state["openai_key"] # クライアント新規作成 client = OpenAI() # client作成後は消す os.environ["OPENAI_API_KEY"] = "" # セッションにセット state["client"] = client else: # 既存のクライアントをセット client = state["client"] if state["thread_id"] == "": # スレッド作成 thread = client.beta.threads.create() state["thread_id"] = thread.id if state["assistant_id"] == "": # 既存のアシスタントがあればIDを取得 assistant_id = get_assist_id(client, ASSIST_NAME) if assistant_id == "": # フォントファイルをアップ font_file_response = client.files.create( purpose="assistants", file=open(FONT_FILE_PATH,"rb"), ) # IDの取得 font_file_id = font_file_response.id # アシスタント新規作成 assistant = client.beta.assistants.create( name=ASSIST_NAME, instructions=AST_SYS_PROMPT, model=DF_MODEL, file_ids=[font_file_id], tools=[{"type": "code_interpreter"}] # テスト中はオフ ) assistant_id = assistant.id state["assistant_id"] = assistant_id # アシスタントIDでフォルダ作成 os.makedirs(state["assistant_id"], exist_ok=True) if file: # ファイル名取得 # basename = os.path.basename(file) # if example_toid.get(basename): # # サンプルの場合は用意したIDをセット # file_id = example_toid.get(basename) # else: # ファイルのアップ file_response = client.files.create( purpose="assistants", file=open(file,"rb"), ) if file_response.status != "processed": # 失敗時 err_msg = "ファイルのアップロードに失敗しました" else: # ファイルのIDをセット file_id = file_response.id except NotFoundError as e: err_msg = "アシスタントIDが間違っています。新しく作成する場合はアシスタントIDを空欄にして下さい。" except AuthenticationError as e: err_msg = "認証エラーとなりました。OpenAPIKeyが正しいか、支払い方法などが設定されているか確認して下さい。" except Exception as e: err_msg = "その他のエラーが発生しました。" print(e) finally: return state, file_id, err_msg def get_assist_id(client, assist_name): assist_id = "" assist_list = client.beta.assistants.list() if len(assist_list.data) == 0: return assist_id for assist in assist_list: if assist.name == assist_name: assist_id = assist.id break return assist_id def raise_exception(err_msg): """ エラーの場合例外を起こす関数 """ if err_msg != "": raise Exception("これは入力チェックでの例外です。") return def add_history(history, text, file_id): """ Chat履歴"history"に追加を行う関数 """ err_msg = "" # 入力テキスト追加 history = history + [(text, None)] # テキスト・ファイルを初期化し利用不可に update_text = gr.update(value="", placeholder = "",interactive=False) update_file = gr.update(value=None, interactive=False) return history, update_text, update_file, err_msg def bot(state, history, file_id): err_msg = "" image_file = None ant_file = None # セッション情報取得 system_prompt = state["system_prompt"] client = state["client"] assistant_id = state["assistant_id"] thread_id = state["thread_id"] last_msg_id = state["last_msg_id"] code_mode = state["code_mode"] if file_id is None or file_id == "": # ファイルがない場合 message = client.beta.threads.messages.create( thread_id=thread_id, role="user", content=history[-1][0], ) else: # ファイルがあるときはIDをセット message = client.beta.threads.messages.create( thread_id=thread_id, role="user", content=history[-1][0], file_ids=[file_id] ) print(message) # RUNスタート run = client.beta.threads.runs.create( thread_id=thread_id, assistant_id=assistant_id, # instructions=system_prompt ) # "completed"となるまで繰り返す for i in range(0, MAX_TRIAL, 1): if i > 0: time.sleep(INTER_SEC) # 指定秒待つ # メッセージ受け取り run = client.beta.threads.runs.retrieve( thread_id=thread_id, run_id=run.id ) # 前回のメッセージより後を昇順で取り出す messages = client.beta.threads.messages.list( thread_id=thread_id, after=last_msg_id, order="asc" ) # デバッグ用 print(run.status) print(messages) # messageを取り出す for msg in messages: if msg.role == "assistant": for content in msg.content: res_text = "" file_id = "" ant_file = None cont_dict = content.model_dump() # 辞書型に変換 ct_image_file = cont_dict.get("image_file") if ct_image_file: # imageファイルがあるならIDセット res_file_id = ct_image_file.get("file_id") # ファイルをダウンロード image_file = file_download(client, res_file_id, assistant_id, ".png") if image_file is None: err_msg = "ファイルのダウンロードに失敗しました。" else: res_text = IMG_MSG history = history + [[None, res_text]] # 最終メッセージID更新 last_msg_id = msg.id else: # 返答テキスト取得 res_text = cont_dict["text"].get("value") # 注釈(参照ファイル)ががある場合取得 if len(cont_dict.get("text").get("annotations")) > 0: ct_ant = cont_dict.get("text").get("annotations") if ct_ant[0].get("file_path") is not None: # 参照ファイルのID取得 ant_file_id = ct_ant[0].get("file_path").get("file_id") if ct_ant[0].get("text") is not None: # ファイル形式(拡張子)取得 ext = "." + ct_ant[0].get("text")[ct_ant[0].get("text").rfind('.') + 1:] # ファイルダウンロード ant_file = file_download(client, ant_file_id, assistant_id, ext) if ant_file is None: err_msg = "参照ファイルのダウンロードに失敗しました。" else: # 参照ファイルがある旨のメッセージを追加 res_text = res_text + "\n\n" + ANT_MSG if res_text != "": # Chat画面更新 if history[-1][1] is not None: # 新しい行を追加 history = history + [[None, res_text]] else: history[-1][1] = res_text # 最終メッセージID更新 last_msg_id = msg.id # Chatbotを返す(labelとhistoryを更新) yield gr.Chatbot(label=run.status, value=history), image_file, ant_file, err_msg # メッセージIDを保存 state["last_msg_id"] = last_msg_id # 完了なら終了 if run.status == "completed": if not code_mode: # yield gr.Chatbot(label=run.status ,value=history), image_file, ant_file, err_msg break else: # コードモードがONの場合 run_steps = client.beta.threads.runs.steps.list( thread_id=thread_id, run_id=run.id ) # コードを取得 input_code = get_code(run_steps) if len(input_code) > 0: for code in input_code: code = "[input_code]\n\n" + code # コードを追加 history = history + [[None, code]] yield gr.Chatbot(label=run.status ,value=history), image_file, ant_file, err_msg break elif run.status == "failed": # エラーとして終了 err_msg = "※メッセージ取得に失敗しました。" yield gr.Chatbot(label=run.status ,value=history), image_file, ant_file, err_msg break elif i == MAX_TRIAL: # エラーとして終了 err_msg = "※メッセージ取得の際にタイムアウトしました。" yield gr.Chatbot(label=run.status ,value=history), image_file, ant_file, err_msg break # else: # if i > 3: # # 作業中とわかるようにする # yield gr.Chatbot(label=run.status + " (Request:" + str(i) + ")" ,value=history), image_file, ant_file, err_msg def get_code(run_steps): """ 生成過程のコードを全てを返す """ input_code = [] for data in run_steps.data: if isinstance(data.step_details, ToolCallsStepDetails): # コードが存在するときだけ取得 for tool_call in data.step_details.tool_calls: input_code.append(tool_call.code_interpreter.input) return input_code def file_download(client, file_id, folder, ext): """ OpenAIからファイルをダウンロードしてパスを返す """ api_response = client.files.with_raw_response.retrieve_content(file_id) if api_response.status_code == 200: content = api_response.content file_path = folder + "/" + file_id + ext with open(file_path, 'wb') as f: f.write(content) return file_path else: return None def finally_proc(): """ 最終処理用関数 """ # テキスト・ファイルを使えるように interactive = gr.update(interactive = True) # ファイルIDはリセット new_file_id = gr.Textbox(value="") return interactive, interactive, new_file_id def clear_click(state): """ クリアボタンクリック時 """ # セッションの一部をリセット() # state["assistant_id"] = "" state["thread_id"] = "" state["last_msg_id"] = "" return state # 画面構成 with gr.Blocks() as demo: title = "

GPT Code Interpreter対応チャット

" message = "・設定タブからAPIKEYを入力してください
" message += "・こういうときにエラーになるなどフィードバックあればお待ちしています。
" message += "・コードインタープリターAPIの解説動画はこちらです→" message += "https://www.youtube.com/watch?v=tFmedAM1FM8
" message += '※動いているかわかりづらいですが、左上の"in_progress(Request:XX)"が止まっていなければ回答の生成中となります。
' message += "※グラフの日本語文字化けの対応方法を注意事項に記載しました。
" # title = "

Code Interpreterデモアプリ

" # message = "

最初に[設定]タブからOpenAIキーを入力してください。" # message += "

" gr.Markdown(title + "

" + message + "

") # セッションの宣言 state = gr.State({ "system_prompt": "", "openai_key" : "", "code_mode" : False, "client" : None, "assistant_id" : "", "thread_id" : "", "last_msg_id" : "" }) with gr.Tab("Chat画面") as chat: # 各コンポーネント定義 chatbot = gr.Chatbot(label="チャット画面") text_msg = gr.Textbox(label="テキスト", placeholder = PLACEHOLDER) with gr.Row(): up_file = gr.File(label="ファイルアップロード", type="filepath",interactive = True) result_image = gr.Image(label="出力画像", type="filepath", interactive = False) gr.Examples(label="サンプルデータ", examples=examples, inputs=[up_file]) with gr.Row(): btn = gr.Button(value="送信") # btn_download = gr.Button(value="画像のダウンロード") # 保留中 # btn_clear = gr.ClearButton(value="リセット", components=[chatbot, text_msg, up_file, file_id, result_image, sys_msg, result_file]) btn_clear = gr.ClearButton(value="リセット", components=[chatbot, text_msg, up_file, result_image]) sys_msg = gr.Textbox(label="システムメッセージ", interactive = False) result_file = gr.File(label="出力ファイル", type="filepath",interactive = False) # ファイルID保存用 file_id = gr.Textbox(visible=False) # 送信ボタンクリック時の処理 bc = btn.click(init, [state, text_msg, up_file], [state, file_id, sys_msg], queue=False).success( raise_exception, sys_msg, None).success( add_history, [chatbot, text_msg, file_id], [chatbot, text_msg, up_file, sys_msg], queue=False).success( bot, [state, chatbot, file_id],[chatbot, result_image, result_file, sys_msg]).then( finally_proc, None, [text_msg, up_file, file_id], queue=False ) # クリア時でもセッションの設定(OpenAIKeyなどは残す) btn_clear.click(clear_click, state, state) # テキスト入力Enter時の処理 # txt_msg = text_msg.submit(respond, inputs=[text_msg, image, chatbot], outputs=[text_msg, image, chatbot]) with gr.Tab("設定") as set: openai_key = gr.Textbox(label="OpenAI API Key", visible=True) # language = gr.Dropdown(choices=["Japanese", "English"], value = "Japanese", label="Language", interactive = True) system_prompt = gr.Textbox(value = "",lines = 5, label="Custom instructions", interactive = True, visible=False) code_output = gr.Dropdown(label="コード出力", choices=["OFF", "ON"], value = "OFF", interactive = True) # 設定タブからChatタブに戻った時の処理 chat.select(set_state, [openai_key, system_prompt, code_output, state], state) with gr.Tab("注意事項") as notes: caution = "現在Assistant APIはβ版でのリリースとなっています。
" # caution += "そのためか一部のファイルのアップロードが上手くいかないため、制限をかけています。
" # caution += "(現在アップできるファイル形式は.txtと.csvのみ)
" # caution += "本来はPDFなども利用できるはずなので、今後更新したいと思います。
" caution += "文字化けする場合「NotoSansJP-Bold.zipを解凍してフォントを取得して下さい。」と指示し
" caution += "グラフ作成時に「フォントはNotoSansJP-Boldを使用して下さい。」と指示して下さい。
" caution += "※NotoSansJP-Bold.zipをアップする必要はありません。
" caution += "詳細はこちら→https://github.com/nekoniii3/openai_multi_chat/tree/main/Code_Interpreter/docs/%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%83%95%E3%82%A9%E3%83%B3%E3%83%88%E5%88%A9%E7%94%A8" gr.Markdown("

" + caution + "

") if __name__ == "__main__": demo.queue() demo.launch(debug=True)