import inspect import logging import uuid from typing import List, Dict import gradio as gr from core.conversation import ConversationEngine logging.basicConfig(level=logging.INFO) try: engine = ConversationEngine() engine_error = None except Exception as exc: # noqa: BLE001 engine = None engine_error = str(exc) logging.exception("Engine initialization failed: %s", exc) def init_state(): sid = engine.session_manager.new_session_id() if engine else str(uuid.uuid4()) return {"session_id": sid, "pending": False} def _normalize_choice(choice_value: str | None) -> str | None: if not choice_value: return None value = choice_value.strip() return value.split(":")[0].strip() if ":" in value else value def _extract_text(item) -> str: """Recursively extract plain text from any Gradio message format.""" if item is None: return "" if isinstance(item, str): return item if isinstance(item, list): for elem in item: text = _extract_text(elem) if text: return text return "" if isinstance(item, dict): for key in ("content", "text", "value"): if key in item: text = _extract_text(item[key]) if text: return text return "" if hasattr(item, "content"): return _extract_text(getattr(item, "content")) return str(item) def _ensure_messages_format(history: List) -> List[Dict[str, str]]: """Convert any history format to messages format: [{"role": "user", "content": "..."}, ...]""" if not history: return [] messages: List[Dict[str, str]] = [] for item in history: if isinstance(item, dict) and "role" in item and "content" in item: # Already messages format - extract clean text content = _extract_text(item.get("content")) if content: messages.append({"role": str(item["role"]), "content": content}) elif hasattr(item, "role") and hasattr(item, "content"): # ChatMessage object content = _extract_text(getattr(item, "content")) if content: messages.append({"role": str(getattr(item, "role")), "content": content}) elif isinstance(item, (list, tuple)) and len(item) == 2: # Tuple format (user, assistant) user_text = _extract_text(item[0]) bot_text = _extract_text(item[1]) if user_text: messages.append({"role": "user", "content": user_text}) if bot_text: messages.append({"role": "assistant", "content": bot_text}) return messages def _append_message(history: List[Dict], role: str, content: str) -> List[Dict]: """Append a message in messages format.""" new_history = list(history) if content: new_history.append({"role": role, "content": content}) return new_history def respond( user_message: str, chat_history: List, app_state: dict, choice_value: str | None, ): """Main response handler using messages format.""" state = app_state or init_state() session_id = state.get("session_id") or init_state()["session_id"] state["session_id"] = session_id # Normalize history to clean messages format history = _ensure_messages_format(chat_history) if engine is None: reply = f"初期化エラー: {engine_error}. OPENAI_API_KEY を確認してください。" history = _append_message(history, "user", user_message or "(入力なし)") history = _append_message(history, "assistant", reply) return history, state, "", gr.update(choices=[], value=None, visible=False), [], {} pending = state.get("pending", False) citations = [] trace = {} if pending: # Handle clarifying question response choice_id = _normalize_choice(choice_value) or _normalize_choice(user_message) if not choice_id: return history, state, "", gr.update(visible=True), citations, trace result = engine.handle_clarifying_answer(session_id, choice_id) user_bubble = f"Clarifying回答: {choice_id}" else: if not user_message: warn = "質問を入力してください。" history = _append_message(history, "user", "(入力なし)") history = _append_message(history, "assistant", warn) return history, state, "", gr.update(choices=[], value=None, visible=False), citations, trace result = engine.handle_user_message(session_id, user_message) user_bubble = user_message if result.get("type") == "clarify": state["pending"] = True options = [f"{c.id}: {c.text}" for c in result["question"].choices] trace = result.get("trace", {}) history = _append_message(history, "user", user_bubble) history = _append_message(history, "assistant", result["reply"]) return ( history, state, "", gr.update(choices=options, value=None, visible=True), citations, trace, ) if result.get("type") == "answer": state["pending"] = False citations = result.get("citations", []) trace = result.get("trace", {}) history = _append_message(history, "user", user_bubble) history = _append_message(history, "assistant", result["reply"]) return ( history, state, "", gr.update(choices=[], value=None, visible=False), citations, trace, ) # Error fallback history = _append_message(history, "user", user_bubble) history = _append_message(history, "assistant", result.get("reply", "エラーが発生しました。")) state["pending"] = False trace = result.get("trace", {}) return history, state, "", gr.update(choices=[], value=None, visible=False), citations, trace def reset(): new_state = init_state() return [], new_state, "", gr.update(choices=[], value=None, visible=False), [], {} with gr.Blocks() as demo: gr.HTML( """
シックデイ、低血糖、足病変などの場面別 Q&A を日本語で案内します。診断・治療を決める用途ではなく、受診判断やセルフケアの補助として利用してください。