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"