diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..5c6e19579f005b3b0cb86f2e58db69d742f7a360 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +--- +title: FAQ Workshop +emoji: 💻 +colorFrom: gray +colorTo: purple +sdk: gradio +sdk_version: 5.16.0 +python_version: 3.11.11 +app_file: app.py +pinned: false +license: mit +short_description: FAQ CHAT by プロジェクトマネジメント勉強会 +--- + +Check out the configuration reference at diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..96f710977efdae9fadc218d617efb302ea455ed0 --- /dev/null +++ b/app.py @@ -0,0 +1,299 @@ +import base64 +import os +import shutil +import threading +import time + +import gradio as gr + +from blog_class import audio_text, knowledge_class, save_feedback + +knowledge_cls = knowledge_class() + + +def update_knowledge(): + knowledge_cls.get_new_knowledge() + choices_list = list(knowledge_cls.reference_dict.keys()) + + return gr.update(choices=choices_list) + + +def user_message_fn(user_message, history): + if history is None: + history = [] + history.append({"role": "user", "content": user_message}) + return "", history + + +def answer_question(history): + print("chat Start") + answer_text, info_title, audio_file_path = knowledge_cls.get_chat_answer(history) + if not answer_text: + yield history + return + audio_answer_container = {"result": None} + + def run_audio_answer(): + audio_answer_container["result"] = audio_text(answer_text, audio_file_path) + + audio_thread = threading.Thread(target=run_audio_answer) + audio_thread.start() + + history.append({"role": "assistant", "content": ""}) + # + history.append( + { + "role": "assistant", + "content": "音声作成中...", + "metadata": {"title": "🎧 音声返信", "status": "pending"}, + } + ) + + history.append( + { + "role": "assistant", + "content": info_title, + "metadata": {"title": "📚 参照ナレッジ", "status": "done"}, + } + ) + + for char in answer_text: + history[-3]["content"] += char + if ( + audio_answer_container["result"] is not None + and history[-2]["metadata"]["status"] != "done" + ): + + audio_str = base64.b64encode( + open(audio_answer_container["result"], "rb").read() + ).decode("utf-8") + + history[-2]["content"] = gr.HTML( + f""" + + """ + ) + + history[-2]["metadata"] = {"title": "🎧 音声返信", "status": "done"} + + yield history + time.sleep(0.03) + while audio_thread.is_alive(): + yield history + time.sleep(0.2) + audio_thread.join() + print("chat end") + if history[-2]["metadata"]["status"] != "done": + audio_str = base64.b64encode(open(audio_file_path, "rb").read()).decode("utf-8") + history[-2]["content"] = gr.HTML( + f""" + + """ + ) + history[-2]["metadata"] = {"title": "🎧 音声返信", "status": "done"} + yield history + + +def handle_feedback(like_data: gr.LikeData): + save_feedback(like_data.value[0], like_data.liked) + + +def get_reference_info(selected_reference): + reference_dict = knowledge_cls.reference_dict.get(selected_reference, None) + if reference_dict: + yield ( + reference_dict["audio"], + reference_dict["original_text"], + reference_dict["summary"], + ) + else: + yield (None, None, None) + + +def zip_directory(): + zip_path = "resource.zip" + if os.path.exists(zip_path): + os.remove(zip_path) + shutil.make_archive(zip_path.replace(".zip", ""), "zip", "./resource") + return zip_path + + +custom_css = """ +body { + background-color: #eef2f7; + margin: 0; + padding: 0; + font-family: "Microsoft YaHei", sans-serif; +} + +#title-area { + text-align: center; + margin-top: 30px; + margin-bottom: 15px; + color: #333; +} + +#chatbot-container, #references-container, #download-container { + max-width: 1100px; + margin: auto; + background: #f0f7ff; +} + +#chatbot-container { + border-radius: 12px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + padding: 10px; +} + +#references-container { + border-radius: 12px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + padding: 15px; +} + +#submit-btn button { + background-color: #007bff; + color: white; + border-radius: 8px; + padding: 8px 12px; +} + +#update-btn button { + background-color: #28a745; + color: white; + border-radius: 8px; + padding: 8px 12px; +} + +h3 { + color: #444; + border-bottom: 2px solid #eee; + padding-bottom: 8px; + margin-bottom: 20px; +} +""" +custom_head = """ + + +""" + +js_func = """ +function refresh() { + const url = new URL(window.location); + + if (url.searchParams.get('__theme') !== 'light') { + url.searchParams.set('__theme', 'light'); + window.location.href = url.href; + } +} +""" +with gr.Blocks(title="勉強会", css=custom_css, head=custom_head, js=js_func) as demo: + + with gr.Column(elem_id="title-area"): + gr.HTML( + """ +
+ +

+ + プロジェクトマネジメント勉強会 + +

+
+ """ + ) + gr.HTML( + """ +
+
+

経験上、プロジェクトマネジメントで大切だと思っている考え方で

+

疑問に答え、音声、参考資料が提供されます

+
+ +
+ """ + ) + + with gr.Column(elem_id="chatbot-container"): + gr.HTML("

何かお困りですか?

") + chatbot = gr.Chatbot( + show_label=False, + type="messages", + bubble_full_width=False, + avatar_images=( + None, + "https://cdn.profile-image.st-hatena.com/users/saratoga623/profile.png?1728512391", + ), + ) + + with gr.Row(elem_id="submit-btn"): + msg = gr.Textbox( + placeholder="質問を入力してEnter...", + show_label=False, + scale=5, + container=False, + ) + submit_btn = gr.Button(value="送信 🚀", scale=1) + + clear_btn = gr.ClearButton([msg, chatbot], value="クリア 🧹", scale=1) + + with gr.Accordion("ナレッジ一覧", elem_id="references-container", open=False): + with gr.Column(): + with gr.Row(): + gr.HTML("

ナレッジ一覧

") + update_btn = gr.Button(value="🔄 更新", elem_id="update-btn") + references_dropdown = gr.Dropdown( + label="ナレッジを選ぶ", + choices=list(knowledge_cls.reference_dict.keys()), + value=None, + ) + reference_audio = gr.Audio( + label="ナレッジ要約を聞く", + type="filepath", + interactive=False, + waveform_options=gr.WaveformOptions( + show_recording_waveform=False, + show_controls=False, + ), + ) + sum_references_box = gr.Textbox( + label="ナレッジ要約", + lines=4, + interactive=False, + ) + with gr.Accordion("ナレッジ详情", open=False): + all_references_box = gr.Markdown(max_height=250) + + with gr.Accordion("資源", open=False, elem_id="download-container"): + download_btn = gr.Button("ダウンロード", elem_id="update-btn") + file_output = gr.File(show_label=False, container=False) + + msg.submit( + fn=user_message_fn, inputs=[msg, chatbot], outputs=[msg, chatbot], queue=False + ).then(fn=answer_question, inputs=chatbot, outputs=chatbot) + + submit_btn.click( + fn=user_message_fn, inputs=[msg, chatbot], outputs=[msg, chatbot], queue=False + ).then(fn=answer_question, inputs=chatbot, outputs=chatbot) + chatbot.like(handle_feedback, None, None, like_user_message=True) + update_btn.click( + fn=update_knowledge, + outputs=references_dropdown, + ) + + references_dropdown.change( + fn=get_reference_info, + inputs=references_dropdown, + outputs=[reference_audio, all_references_box, sum_references_box], + ) + + download_btn.click(fn=zip_directory, outputs=file_output) + +# app +if __name__ == "__main__": + demo.launch(inline=False, share=False, debug=True) diff --git a/blog_class.py b/blog_class.py new file mode 100644 index 0000000000000000000000000000000000000000..4a8512fcbc71f44c46278b112a4053235112b952 --- /dev/null +++ b/blog_class.py @@ -0,0 +1,452 @@ +import os +import pickle +import re +import time +from datetime import datetime +from urllib.parse import urljoin + +import numpy as np +import requests +from bs4 import BeautifulSoup +from fish_audio_sdk import Session, TTSRequest +from openai import OpenAI +from tenacity import retry, stop_after_attempt, wait_exponential + +# params +PKL_FILE = "./resource/knowledge_data.pkl" +INFO_audio_ID = os.getenv("INFO_audio_ID") +BASE_URL = "https://saratoga623.hatenablog.com/" + +client = OpenAI(api_key=os.getenv("gpt")) + +audio_client = Session(os.getenv("audio")) + +SYS_Prompt = """ +あなたは言語の専門家であり、さまざまな話し方や思考パターンを分析し、模倣し、適応することに長けています。 +""" + +SUMMARY_Prompt = """ +あなたは文章の内容を分析する専門家です。 +以下の文章の要約を作成してください。要約は100字以内で簡潔にまとめてください。 +""" + +STYLE_Prompt = """ +あなたは文章の内容を分析する専門家です。 +以下の文章内容を分析して、 +1. 原文の作者の文体を抽出してください。例:敬語、語気、構造、用語など代表的なもの +2. 原文の作者の思考パターンを抽出してください。例:論理構成、推論、態度など代表的なもの +""" +SEGMENT_Prompt = """ +あなたは文章の内容を分析する専門家です。 +以下の文章を内容に基づいて意味的に段落に分割してください。各段落は独立した文章で、段落ごとに改行を入れてください。 +""" + +QA_Prompt = """ +ユーザーの質問に応じて表現を的確に調整し、自然でスムーズな対話を実現し、\ +検索された情報の内容、文脈、コミュニケーションスタイルに適した応答を簡潔に提供してください。 + +【ユーザーの質問】 +{q_text} +【検索された情報】 +{r_text} +【タスク要求】 +- 検索された情報を参考に、質問に会話の形で回答すること。 +- 原文の作者の文体を模倣すること。 +- 原文の思考パターンを模倣すること。 +- 簡潔な回答を心がけること。3-5文程度で要点をまとめることを意識してください。 +- 文章風ではなく、対話形式で答えること。 +""" +REWRITE_SYS_Prompt = """ +Given a conversation (between Human and Assistant) and a follow up message from Human, \ +rewrite the message to be a standalone question that captures all relevant context \ +from the conversation. +""" +REWRITE_Prompt = """ + +{chat_history} + + +{question} + + +""" +QA_chat_Prompt = """ +ユーザーの最新質問に応じて表現を的確に調整し、自然でスムーズな対話を実現し、\ +検索された情報の内容、文脈、コミュニケーションスタイルに適した応答を簡潔に提供してください。 +【ユーザーの会話履歴】 +{h_text} +【ユーザーの最新質問】 +{q_text} +【検索された情報】 +{r_text} +【タスク要求】 +- 検索された情報内容を参考に、質問に会話の形で回答すること。 +- 原文の作者の文体を模倣すること。 +- 原文の作者の思考パターンを模倣すること。 +- 簡潔な回答を心がけること。3-5文程度で要点をまとめることを意識してください。 +- 文章風ではなく、対話形式で答えること。 +""" + + +def load_pkl(file_path): + if os.path.exists(file_path): + with open(file_path, "rb") as f: + return pickle.load(f) + else: + return {} + + +def save_pkl(file_name, data): + with open(file_name, "wb") as f: + pickle.dump(data, f) + + +def get_page_content(page_url): + response = requests.get(page_url) + response.encoding = response.apparent_encoding + return response.text + + +def parse_homepage(html): + + soup = BeautifulSoup(html, "html.parser") + articles = soup.find_all("article", class_="entry") + blog_infos = [] + for article in articles: + title_tag = article.find("h1", class_="entry-title") + if title_tag and title_tag.find("a"): + link = urljoin(BASE_URL, title_tag.find("a")["href"]) + else: + continue + #