Spaces:
Sleeping
Sleeping
import os | |
import time | |
import datetime | |
import gradio as gr | |
from openai import OpenAI | |
from openai.types.beta.threads.runs import ToolCallsStepDetails | |
# GPT用設定 | |
SYS_PROMPT_DEFAULT = "あなたは優秀なアシスタントです。質問をされた場合は、質問に答えるコードを作成して実行します。回答は日本語でお願いします。" | |
DUMMY = "********************" | |
# file_format = {".txt", ".csv", ".pdf"} | |
file_format = {".txt", ".csv"} | |
# 各種出力フォルダ | |
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"] | |
example_toid = {"東京都年別人口.csv" : "file-TMLJxwdTy1oaaoo2l712r1Os" | |
,"世界の人口2023年.csv" : "file-rafHPSkmMciOjKovah0u5oug" | |
, "練馬区年齢別人口.csv" : "file-B5VqVSqgh8dB70ygHwt0LVIH" | |
, "桃太郎あらすじ.txt" : "file-ylcNO5LShDYgDk5wiuSiyTMA"} | |
# コード出力用 | |
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): | |
""" 入力チェックを行う関数 | |
※ここで例外を起こすと入力できなくなるので次の関数でエラーにする """ | |
print(state) | |
err_msg = "" | |
file_id = None | |
# if state["openai_key"] == "" or state["openai_key"] is None: | |
# # OpenAI API Key未入力 | |
# err_msg = "OpenAI API Keyを入力してください。(設定タブ)" | |
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 | |
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"] == "": | |
# アシスタント作成 | |
# assistant = client.beta.assistants.create( | |
# name="codeinter_test", | |
# instructions=state["system_prompt"], | |
# # model="gpt-4-1106-preview", | |
# model="gpt-3.5-turbo-1106", | |
# tools=[{"type": "code_interpreter"}] | |
# ) | |
# state["assistant_id"] = assistant.id | |
state["assistant_id"] = os.environ["ASSIST_ID"] # テスト中アシスタントは固定 | |
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 | |
print(file_id) | |
return state, file_id, err_msg | |
def raise_exception(err_msg): | |
""" エラーの場合例外を起こす関数 """ | |
if err_msg != "": | |
raise Exception("これは入力チェックでの例外です。") | |
return | |
def add_history(history, text, file_id): | |
""" Chat履歴"history"に追加を行う関数 """ | |
err_msg = "" | |
if file_id is None or file_id == "": | |
# テキストだけの場合そのまま追加 | |
history = history + [(text, None)] | |
elif file_id is not None: | |
# ファイルがあればファイルIDとテキストを追加 | |
history = history + [("file:" + file_id, DUMMY)] | |
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" | |
) | |
msg_log = client.beta.threads.messages.list( | |
thread_id=thread_id, | |
# after=last_msg_id, | |
order="asc" | |
) | |
# デバッグ用 | |
print(run.status) | |
print(msg_log) | |
# 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, IMG_FOLDER , ".png") | |
if image_file is None: | |
err_msg = "ファイルのダウンロードに失敗しました。" | |
else: | |
print("画像ファイル追加") | |
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, ANT_FOLDER, 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 = "<h2>GPT Code Interpreter対応チャット</h2>" | |
message = "<h3>・テスト中でAPIKEY無しで動きます ※機密ファイルはアップしないでください<br>" | |
message += "・こういうときにエラーになるなどフィードバックあればお待ちしています。<br>" | |
message += '※動いているかわかりづらいですが、左上の"in_progress(Request:XX)"が止まっていなければ回答の生成中となります。<br>' | |
message += "※グラフの日本語文字化けの対応の仕方を注意事項に記載しました。<br>" | |
message += "・コードインタープリターAPIの解説動画はこちらです→" | |
message += "https://www.youtube.com/watch?v=tFmedAM1FM8<br></h3>" | |
gr.Markdown(title + message) | |
# セッションの宣言 | |
state = gr.State({ | |
"system_prompt": SYS_PROMPT_DEFAULT, | |
"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]) | |
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=False) # テスト中は表示せず | |
# language = gr.Dropdown(choices=["Japanese", "English"], value = "Japanese", label="Language", interactive = True) | |
system_prompt = gr.Textbox(value = SYS_PROMPT_DEFAULT,lines = 5, label="Custom instructions", interactive = True) | |
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はβ版でのリリースとなっています。<br>" | |
caution += "そのためか一部のファイルのアップロードが上手くいかないため、制限をかけています。<br>" | |
caution += "(現在アップできるファイル形式は.txtと.csvのみ)<br>" | |
caution += "本来はPDFなども利用できるはずなので、今後更新したいと思います。<br>" | |
caution += "文字化けする場合「NotoSansJP-Bold.zipを解凍してフォントを取得して下さい。」と指示し<br>" | |
caution += "グラフ作成時に「フォントはNotoSansJP-Boldを使用して下さい。」と指示して下さい。<br>" | |
caution += "※NotoSansJP-Bold.zipをアップする必要はありません。<br>" | |
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("<h3>" + caution + "</h3>") | |
demo.queue() | |
demo.launch(debug=True) | |