MCP_test / app.py
ryoshimu
commit
6d07546
import os
from typing import Any, Dict, List, Tuple
import gradio as gr
from openai import OpenAI
def get_openai_client() -> OpenAI:
"""OpenAI API クライアントを環境変数から初期化して返す。"""
api_key = os.getenv("OPENAI_API_KEY", "").strip()
if not api_key:
raise RuntimeError("OPENAI_API_KEY が設定されていません。環境変数を確認してください。")
base_url = os.getenv("OPENAI_API_BASE", "").strip() or None
return OpenAI(api_key=api_key, base_url=base_url)
def get_vector_store_id() -> str:
"""環境変数 VECTOR_STORE_ID から単一のベクターストアIDを取得する。"""
value = os.getenv("VECTOR_STORE_ID", "").strip()
if not value:
raise RuntimeError("VECTOR_STORE_ID が設定されていません。環境変数を確認してください。")
return value.split(",")[0].strip()
def extract_citations(response_dict: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Responses API レスポンスから file_search の引用情報を抽出する。"""
citations: List[Dict[str, Any]] = []
outputs = response_dict.get("output") or []
for output in outputs:
for content in output.get("content", []) or []:
annotations = content.get("annotations") or []
for annotation in annotations:
file_citation = annotation.get("file_citation")
if not file_citation:
continue
entry = {
"file_id": file_citation.get("file_id"),
"file_name": file_citation.get("file_name"),
"vector_store_id": file_citation.get("vector_store_id"),
"quote": file_citation.get("quote"),
}
if entry not in citations:
citations.append(entry)
return citations
def answer_with_file_search(
vector_store_id: str,
query: str,
top_k: int = 5,
model: str = "gpt-4o-mini",
) -> Tuple[str, List[Dict[str, Any]]]:
"""
Responses API + file_search ツールを用いて検索と回答を同時に行い、回答と引用を返す。
Cookbook の "Integrating search results with LLM in a single API call" を参考に構成。
"""
if not query.strip():
return "", []
client = get_openai_client()
system_prompt = (
"あなたは取得したドキュメントから回答するアシスタントです。"
)
user_message = f"{query}\n\n検索結果は最大 {top_k} 件まで引用してください。"
response = client.responses.create(
model=model,
input=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_message},
],
tools=[
{
"type": "file_search",
"vector_store_ids": [vector_store_id],
}
],
)
try:
response_dict = response.model_dump()
except AttributeError:
# 旧バージョンSDK互換: model_dump が無ければ辞書化 fallback
response_dict = getattr(response, "to_dict", lambda: {})()
answer_text = response_dict.get("output_text") or ""
citations = extract_citations(response_dict)
return answer_text.strip(), citations
def format_answer(answer: str, citations: List[Dict[str, Any]]) -> str:
"""回答文と引用リストを UI 向けのテキストに整形する。"""
if not answer:
return "該当データがありません!"
lines = [answer]
if citations:
lines.append("")
lines.append("引用:")
for citation in citations:
file_name = citation.get("file_name") or citation.get("file_id") or "unknown"
quote = citation.get("quote")
if quote:
lines.append(f"- {quote} [{file_name}]")
else:
lines.append(f"- [{file_name}]")
return "\n".join(lines)
def produce_answer(question: str, top_k: int) -> str:
"""Gradio UI からの質問を処理し、Responses API の結果を返す。"""
question = (question or "").strip()
if not question:
return "質問が入力されていません。"
try:
vector_store_id = get_vector_store_id()
answer, citations = answer_with_file_search(vector_store_id, question, top_k=top_k)
return format_answer(answer, citations)
except RuntimeError as runtime_error:
return str(runtime_error)
except Exception:
return "システムエラーが発生しました。時間をおいて再度お試しください。"
def respond(question: str, top_k: int = 5) -> str:
"""Gradio の UI から呼び出されるハンドラ。"""
try:
return produce_answer(question, int(top_k))
except Exception:
return "システムエラーが発生しました。時間をおいて再度お試しください。"
with gr.Blocks() as demo:
gr.Markdown(
"""
## MCP Storage レスポンダ
OpenAI Responses API の file_search ツールを利用し、指定した Vector Store から根拠付きで回答します。
OPENAI_API_KEY と VECTOR_STORE_ID を環境変数に設定してからご利用ください。
"""
)
with gr.Row():
question_box = gr.Textbox(
label="質問",
placeholder="ここに質問を入力してください(例: 製品Aの対応OSは?)",
lines=4,
)
with gr.Row():
top_k_slider = gr.Slider(
minimum=1,
maximum=10,
value=5,
step=1,
label="検索件数 (top_k)",
)
with gr.Row():
output_box = gr.Textbox(label="回答", lines=10)
with gr.Row():
submit_button = gr.Button("検索")
submit_button.click(respond, inputs=[question_box, top_k_slider], outputs=output_box)
if __name__ == "__main__":
demo.launch()