File size: 8,450 Bytes
8eaa451 dce8f48 8eaa451 ae6f621 8eaa451 ae6f621 8eaa451 ae6f621 8eaa451 1da9f9b 8eaa451 ae6f621 8eaa451 ae6f621 1da9f9b ae6f621 dce8f48 8eaa451 dce8f48 8eaa451 dce8f48 8eaa451 dce8f48 8eaa451 dce8f48 8eaa451 ae6f621 dce8f48 8eaa451 ae6f621 dce8f48 ae6f621 8eaa451 ae6f621 dce8f48 8eaa451 dce8f48 8eaa451 dce8f48 8eaa451 dce8f48 ae6f621 dce8f48 8eaa451 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | """
DeepMed-AI — tools/llm_client.py
LLM client với chiến lược failover:
Primary: Gemini 2.5 Flash (Google API Key)
Fallback: DeepSeek V3.2 via NVIDIA Build (miễn phí) — tự động chuyển khi Gemini hết quota
"""
import os
from app.core.config import GOOGLE_API_KEY, GOOGLE_API_KEYS, NVIDIA_API_KEY, NVIDIA_MODEL, OPENROUTER_API_KEY, OPENROUTER_MODEL
from app.core.logging_config import logger
_llm_instance = None
# ── System Prompt Y Tế Chuyên Biệt (TTYT Thanh Ba) ────────────────────────────
DEEPMED_SYSTEM_PROMPT = """Bạn là **DeepMed-AI** — Trợ lý Y khoa và Dược lâm sàng của **Trung tâm Y tế Khu vực Thanh Ba**, \
tỉnh Phú Thọ, Việt Nam. Bạn hỗ trợ đội ngũ y bác sĩ và dược sĩ trong:
- Tra cứu thông tin thuốc (tên, hoạt chất, **giá**, hãng sản xuất, liều dùng, chống chỉ định...)
- Chẩn đoán, phác đồ điều trị, tư vấn y khoa
- Tra cứu danh mục thuốc nội bộ của TTYT Thanh Ba
## Nguyên tắc bắt buộc:
1. **Chính xác & Trung thực**: Chỉ trả lời dựa trên tài liệu Context. \
Nếu Context không có thông tin, hãy nói rõ: "Trong kho dữ liệu hiện tại chưa có thông tin về vấn đề này", \
tuyệt đối không tự bịa phác đồ hay thông tin thuốc.
2. **Giá thuốc**: Khi tài liệu nội bộ có thông tin giá (ví dụ "Giá của thuốc X: 21.798"), \
hãy TRẢ LỜI giá đó. KHÔNG được nói "tôi không cung cấp thông tin giá". \
Đây là thông tin danh mục thuốc nội bộ, KHÔNG phải thông tin thương mại.
3. **Trích dẫn đúng nguồn**: Nếu nhắc đến số liệu hoặc quy định cụ thể, \
hãy nhắc nhẹ tên tài liệu (đã ghi ở phần [Trích từ tài liệu:...]). KHÔNG bịa tên tài liệu.
4. **Văn phong**: Hãy trình bày như một *đồng nghiệp* đang trao đổi chuyên môn: \
tự nhiên, trôi chảy, dễ hiểu. In đậm từ khóa quan trọng, dùng gạch đầu dòng khi cần. \
Trả lời bằng tiếng Việt, kèm thuật ngữ Latin/Anh trong ngoặc nếu cần.
5. **Quy tắc an toàn**:
- KHÔNG BAO GIỜ lấy thông tin thuốc A gán cho thuốc B
- Không chẩn đoán thay bác sĩ — chỉ hỗ trợ tra cứu
- Cấp cứu: hướng dẫn sơ cứu + gọi 115
6. **Xác định thuốc đúng**: Mỗi tài liệu bắt đầu bằng header `[Thuốc: TÊN | Hoạt chất: ...]`. \
Dùng header này để xác định tài liệu thuộc về thuốc nào. Nếu câu hỏi về thuốc X mà tài liệu \
là về thuốc Y → BỎ QUA tài liệu đó.
7. **Bối cảnh**: Ưu tiên phác đồ Bộ Y tế Việt Nam, WHO, thực tế tuyến huyện.
8. **Luôn kết thúc bằng**: *Thông tin mang tính chất tham khảo từ dữ liệu nội bộ. \
Quyết định điều trị thuộc về bác sĩ chuyên khoa.*"""
def _build_gemini_llm():
"""Build Gemini 2.5 Flash LLM instance (first available key)."""
if not GOOGLE_API_KEYS:
return None
try:
from langchain_google_genai import ChatGoogleGenerativeAI
llm = ChatGoogleGenerativeAI(
model="gemini-2.5-flash",
google_api_key=GOOGLE_API_KEYS[0],
temperature=0.2,
max_output_tokens=4096,
thinking={"thinking_budget": 1024},
)
logger.info("✅ Gemini 2.5 Flash initialized (key 1/%d)", len(GOOGLE_API_KEYS))
return llm
except Exception as e:
logger.error("❌ Gemini init failed: %s", e)
return None
def _build_gemini_rotation() -> list:
"""Build a list of Gemini LLM instances — one per API key for rotation."""
if len(GOOGLE_API_KEYS) <= 1:
return []
from langchain_google_genai import ChatGoogleGenerativeAI
extras = []
for i, key in enumerate(GOOGLE_API_KEYS[1:], start=2):
try:
llm = ChatGoogleGenerativeAI(
model="gemini-2.5-flash",
google_api_key=key,
temperature=0.2,
max_output_tokens=4096,
thinking={"thinking_budget": 1024},
)
extras.append(llm)
logger.info("✅ Gemini 2.5 Flash key %d/%d initialized", i, len(GOOGLE_API_KEYS))
except Exception as e:
logger.warning("⚠️ Gemini key %d init failed: %s", i, e)
return extras
def _build_nvidia_llm():
"""Build DeepSeek V3.2 LLM via NVIDIA Build (OpenAI-compatible API, free)."""
if not NVIDIA_API_KEY:
return None
try:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model=NVIDIA_MODEL,
openai_api_key=NVIDIA_API_KEY,
openai_api_base="https://integrate.api.nvidia.com/v1",
temperature=0.2,
max_tokens=4096,
)
logger.info("✅ %s via NVIDIA Build initialized (fallback)", NVIDIA_MODEL)
return llm
except Exception as e:
logger.error("❌ NVIDIA Build init failed: %s", e)
return None
def _build_openrouter_llm():
"""Build LLM via OpenRouter (legacy fallback)."""
if not OPENROUTER_API_KEY:
return None
try:
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
model=OPENROUTER_MODEL,
openai_api_key=OPENROUTER_API_KEY,
openai_api_base="https://openrouter.ai/api/v1",
temperature=0.2,
max_tokens=4096,
default_headers={
"HTTP-Referer": "https://deepmed-ai.hf.space",
"X-Title": "DeepMed-AI",
},
)
logger.info("✅ %s via OpenRouter initialized (fallback)", OPENROUTER_MODEL)
return llm
except Exception as e:
logger.error("❌ OpenRouter init failed: %s", e)
return None
def get_llm():
"""Return LLM with failover: Gemini → NVIDIA Build → OpenRouter.
Uses LangChain's .with_fallbacks() so when Gemini hits quota/rate-limit
errors, requests automatically route to fallback — no manual switching needed.
"""
global _llm_instance
if _llm_instance is not None:
return _llm_instance
gemini = _build_gemini_llm()
gemini_extras = _build_gemini_rotation()
nvidia = _build_nvidia_llm()
openrouter = _build_openrouter_llm()
# Build fallback chain: Gemini key1 → key2 → key3 → NVIDIA → OpenRouter
fallbacks = gemini_extras + [fb for fb in [nvidia, openrouter] if fb is not None]
if gemini and fallbacks:
_llm_instance = gemini.with_fallbacks(fallbacks)
n_keys = len(GOOGLE_API_KEYS)
ext_fb = _fallback_name()
if n_keys > 1:
logger.info("🔄 LLM failover: Gemini ×%d keys → %s", n_keys, ext_fb)
else:
logger.info("🔄 LLM failover: Gemini 2.5 Flash → %s", ext_fb)
elif gemini:
if gemini_extras:
_llm_instance = gemini.with_fallbacks(gemini_extras)
logger.info("🟢 LLM: Gemini 2.5 Flash ×%d keys (no external fallback)", len(GOOGLE_API_KEYS))
else:
_llm_instance = gemini
logger.info("🟢 LLM: Gemini 2.5 Flash only (no fallback configured)")
elif fallbacks:
_llm_instance = fallbacks[0] if len(fallbacks) == 1 else fallbacks[0].with_fallbacks(fallbacks[1:])
logger.info("🟡 LLM: %s only (no Gemini key)", _fallback_name())
else:
logger.error("❌ Không có LLM nào khả dụng! Cần GOOGLE_API_KEY hoặc NVIDIA_API_KEY")
return None
return _llm_instance
def _fallback_name() -> str:
"""Return human-readable fallback name."""
parts = []
if NVIDIA_API_KEY:
parts.append(f"{NVIDIA_MODEL} (NVIDIA)")
if OPENROUTER_API_KEY:
parts.append(f"{OPENROUTER_MODEL} (OpenRouter)")
return " → ".join(parts) or "none"
def get_llm_name() -> str:
"""Return human-readable name of the active LLM configuration."""
fb = _fallback_name()
n_keys = len(GOOGLE_API_KEYS)
gemini_label = f"Gemini 2.5 Flash ×{n_keys}" if n_keys > 1 else "Gemini 2.5 Flash"
if GOOGLE_API_KEYS and fb:
return f"{gemini_label} → {fb}"
elif GOOGLE_API_KEYS:
return gemini_label
elif fb:
return fb
return "Không có LLM"
|