| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664 |
- # -*- coding: utf-8 -*-
- """
- Production Diagnostic Script
- Deploy to server and run to trace the full RAG pipeline for any query.
- Usage:
- python diagnose_online.py "青霉素过敏处理"
- python diagnose_online.py "青霉素过敏处理" --top-k 30
- python diagnose_online.py "青霉素过敏处理" --call-api # also call the API
- """
- import sys
- import os
- import json
- import re
- import asyncio
- import argparse
- import math
- from pathlib import Path
- # ============================================================
- # Environment Setup
- # ============================================================
- def load_env():
- """Load .env file from project root."""
- candidates = [
- Path(__file__).resolve().parent / ".env",
- Path.cwd() / ".env",
- Path("/opt/pharmacopoeia-ai/.env"),
- ]
- for env_file in candidates:
- if env_file.exists():
- with open(env_file, encoding="utf-8") as f:
- for line in f:
- line = line.strip()
- if line and not line.startswith("#") and "=" in line:
- key, _, val = line.partition("=")
- os.environ.setdefault(key.strip(), val.strip())
- print(f"[ENV] Loaded from {env_file}")
- return True
- print("[ENV] WARNING: No .env file found, using env vars directly")
- return False
- load_env()
- # ============================================================
- # Intent Classification (copy from retriever.py)
- # ============================================================
- def classify_intent(query: str) -> str:
- q = query.strip()
- negation_patterns = [
- r"不是", r"没有", r"并非", r"不算", r"不属于",
- r"这不是", r"我没有", r"不包含", r"不涉及",
- ]
- if any(re.search(pat, q) for pat in negation_patterns):
- return "drug_query"
- usage_keywords = [
- "怎么吃", "吃多少", "怎么用", "一天几次", "多长时间",
- "能一起吃", "孕妇能用", "儿童用量", "哺乳期",
- "饭前还是饭后", "空腹", "过量", "漏服", "停药",
- "副作用多大", "伤肝吗", "伤肾吗", "安全吗",
- ]
- safety_sections = [
- "副作用", "不良反应", "禁忌", "注意事项",
- "能不能", "可以吗", "会不会",
- ]
- regulation_keywords = [
- "凡例", "通则规定", "制剂通则",
- "一般规定", "通用技术要求", "检验方法通则",
- ]
- exam_keywords = [
- "执业药师考试", "考点", "历年真题", "考试大纲",
- "高频考点", "报名时间",
- ]
- symptom_keywords = [
- "吃了什么药", "吃什么药", "该吃", "推荐用药", "推荐下用药",
- "买什么药", "推荐什么药", "用什么药", "用药建议",
- "发烧", "咳嗽", "感冒", "腹泻", "头疼", "头痛",
- "嗓子疼", "流鼻涕", "鼻塞", "肚子疼", "胃疼",
- "过敏", "皮肤痒", "失眠", "便秘", "牙疼",
- "体温", "多少度", "退烧", "止痛", "止泻",
- ]
- if any(kw in q for kw in usage_keywords):
- return "usage_guide"
- if any(kw in q for kw in safety_sections):
- return "usage_guide"
- if any(kw in q for kw in symptom_keywords):
- return "symptom_advice"
- if any(kw in q for kw in regulation_keywords):
- return "regulation"
- if any(kw in q for kw in exam_keywords):
- return "exam_tutor"
- return "drug_query"
- # ============================================================
- # Tokenization (copy from reranker.py)
- # ============================================================
- def tokenize_query(query: str) -> set:
- tokens = set()
- for n in range(2, 5):
- for i in range(len(query) - n + 1):
- seg = query[i:i + n]
- if all('一' <= c <= '鿿' for c in seg):
- tokens.add(seg)
- for word in re.findall(r'[a-zA-Z0-9]+', query):
- tokens.add(word.lower())
- return tokens
- def keyword_overlap_score(query_terms: set, content: str) -> float:
- if not query_terms:
- return 0.0
- matched = sum(1 for t in query_terms if t in content)
- return matched / len(query_terms)
- # ============================================================
- # Database Connection & Vector Search
- # ============================================================
- def get_db_url() -> str:
- host = os.environ.get("POSTGRES_HOST", "localhost")
- port = os.environ.get("POSTGRES_PORT", "5432")
- db = os.environ.get("POSTGRES_DB", "pharmacopoeia")
- user = os.environ.get("POSTGRES_USER", "postgres")
- password = os.environ.get("POSTGRES_PASSWORD", "postgres")
- return f"postgresql+asyncpg://{user}:{password}@{host}:{port}/{db}"
- async def get_query_embedding(query: str) -> list:
- """Call DashScope embedding API."""
- import httpx
- api_key = os.environ.get("QWEN_API_KEY", "")
- if not api_key:
- raise RuntimeError("QWEN_API_KEY not set")
- url = "https://dashscope.aliyuncs.com/api/v1/services/embeddings/text-embedding/text-embedding"
- async with httpx.AsyncClient(timeout=30) as client:
- resp = await client.post(
- url,
- headers={
- "Content-Type": "application/json",
- "Authorization": f"Bearer {api_key}",
- },
- json={
- "model": "text-embedding-v3",
- "input": {"texts": [query]},
- "parameters": {"text_type": "query"},
- },
- )
- data = resp.json()
- if data.get("code") and data.get("code") != "":
- raise RuntimeError(f"Embedding API error: {data.get('message')}")
- return data["output"]["embeddings"][0]["embedding"]
- async def vector_search(query_vec: list, top_k: int = 20) -> list:
- """Run pgvector cosine similarity search."""
- from sqlalchemy.ext.asyncio import create_async_engine
- from sqlalchemy import text
- db_url = get_db_url()
- vec_str = "[" + ",".join(str(v) for v in query_vec) + "]"
- engine = create_async_engine(db_url, pool_size=5, max_overflow=10, pool_pre_ping=True)
- try:
- async with engine.connect() as conn:
- result = await conn.execute(
- text("""
- SELECT content, source, drug_id, section,
- 1 - (vec <=> CAST(:qv AS vector)) AS similarity
- FROM drug_chunks
- WHERE vec IS NOT NULL
- ORDER BY vec <=> CAST(:qv AS vector)
- LIMIT :k
- """),
- {"qv": vec_str, "k": top_k},
- )
- rows = result.fetchall()
- docs = []
- for row in rows:
- docs.append({
- "content": row[0],
- "source": row[1],
- "drug_id": row[2],
- "section": row[3],
- "score": float(row[4]),
- })
- return docs
- finally:
- await engine.dispose()
- async def get_db_stats() -> dict:
- """Get statistics about the drug_chunks table."""
- from sqlalchemy.ext.asyncio import create_async_engine
- from sqlalchemy import text
- db_url = get_db_url()
- engine = create_async_engine(db_url)
- try:
- async with engine.connect() as conn:
- # Total chunks
- r = await conn.execute(text("SELECT COUNT(*) FROM drug_chunks WHERE vec IS NOT NULL"))
- total_chunks = r.fetchone()[0]
- # By source
- r = await conn.execute(text("""
- SELECT source, COUNT(*) as cnt
- FROM drug_chunks
- WHERE vec IS NOT NULL
- GROUP BY source
- ORDER BY cnt DESC
- LIMIT 20
- """))
- by_source = [(row[0], row[1]) for row in r.fetchall()]
- # By section
- r = await conn.execute(text("""
- SELECT section, COUNT(*) as cnt
- FROM drug_chunks
- WHERE vec IS NOT NULL
- GROUP BY section
- ORDER BY cnt DESC
- LIMIT 20
- """))
- by_section = [(row[0], row[1]) for row in r.fetchall()]
- # Drugs with penicillin in name
- r = await conn.execute(text("""
- SELECT DISTINCT d.name, d.category, d.source_volume
- FROM drugs d
- WHERE d.name LIKE '%青霉素%'
- """))
- penicillin_drugs = [(row[0], row[1], row[2]) for row in r.fetchall()]
- # Drugs with 清开灵 in name
- r = await conn.execute(text("""
- SELECT DISTINCT d.name, d.category, d.source_volume
- FROM drugs d
- WHERE d.name LIKE '%清开灵%'
- """))
- qkl_drugs = [(row[0], row[1], row[2]) for row in r.fetchall()]
- # Count drugs that have allergy in their sections
- r = await conn.execute(text("""
- SELECT COUNT(DISTINCT drug_id)
- FROM drug_chunks
- WHERE content LIKE '%过敏%' AND vec IS NOT NULL
- """))
- allergy_chunk_count = r.fetchone()[0]
- # Count total unique drugs
- r = await conn.execute(text("SELECT COUNT(*) FROM drugs WHERE is_active = TRUE"))
- total_drugs = r.fetchone()[0]
- return {
- "total_chunks": total_chunks,
- "total_drugs": total_drugs,
- "allergy_chunk_count": allergy_chunk_count,
- "by_source": by_source,
- "by_section": by_section,
- "penicillin_drugs": penicillin_drugs,
- "qkl_drugs": qkl_drugs,
- }
- finally:
- await engine.dispose()
- # ============================================================
- # Reranker Simulation
- # ============================================================
- def simulate_reranker(query: str, documents: list, top_k: int = 5) -> list:
- """Simulate the reranker behavior on retrieval results."""
- query_terms = tokenize_query(query)
- MIN_THRESHOLD = 0.3
- # Step 1: Filter low similarity
- filtered = [d for d in documents if d.get("score", 0) >= MIN_THRESHOLD]
- # Step 2: Keyword bonus
- for doc in filtered:
- content = doc.get("content", "")
- bonus = keyword_overlap_score(query_terms, content)
- doc["keyword_score"] = bonus
- doc["fused_score"] = doc.get("score", 0) + bonus * 0.3
- # Step 3: Sort by fused score
- sorted_docs = sorted(filtered, key=lambda d: d.get("fused_score", 0), reverse=True)
- # Step 4: Dedup
- deduped = []
- seen_texts = []
- for doc in sorted_docs:
- content = doc.get("content", "")
- # Simple dedup: check first 200 chars
- sample = set(content[:200])
- is_dup = False
- for seen in seen_texts[-5:]:
- seen_sample = set(seen[:200])
- intersection = len(sample & seen_sample)
- union = len(sample | seen_sample)
- if union > 0 and intersection / union > 0.8:
- is_dup = True
- break
- if not is_dup:
- deduped.append(doc)
- seen_texts.append(content)
- return deduped[:top_k]
- # ============================================================
- # Extract drug name from chunk content
- # ============================================================
- def extract_drug_name(content: str) -> str:
- m = re.match(r'【(.+?) - ', content)
- return m.group(1) if m else "unknown"
- # ============================================================
- # API Caller
- # ============================================================
- async def call_production_api(query: str, api_base: str = "https://pharmacopoeia.kailin.com.cn"):
- """Call the production chat API and capture full SSE response."""
- import httpx
- # Step 1: Get guest token
- print(f"\n[API] Getting guest token from {api_base}/api/v1/auth/guest ...")
- async with httpx.AsyncClient(timeout=30) as client:
- resp = await client.post(f"{api_base}/api/v1/auth/guest")
- if resp.status_code != 200:
- print(f" FAILED: {resp.status_code} {resp.text}")
- return None
- token_data = resp.json()
- token = token_data.get("access_token", "")
- print(f" OK: token={token[:20]}...")
- # Step 2: Call chat stream
- print(f"\n[API] Calling {api_base}/api/v1/chat/stream ...")
- print(f" Query: {query}")
- events = {"intent": None, "status": [], "content": [], "meta": None}
- async with client.stream(
- "POST",
- f"{api_base}/api/v1/chat/stream",
- headers={
- "Content-Type": "application/json",
- "Authorization": f"Bearer {token}",
- },
- json={"message": query, "conversation_id": f"diag-{os.urandom(4).hex()}"},
- ) as stream:
- current_event = None
- async for line in stream.aiter_lines():
- if line.startswith("event: "):
- current_event = line[7:].strip()
- elif line.startswith("data: "):
- data = line[6:]
- if current_event == "intent":
- events["intent"] = data
- print(f" [SSE] intent: {data}")
- elif current_event == "status":
- events["status"].append(data)
- print(f" [SSE] status: {data}")
- elif current_event == "content":
- if data != "[DONE]":
- events["content"].append(data)
- elif current_event == "meta":
- try:
- events["meta"] = json.loads(data)
- print(f" [SSE] meta: sources={len(events['meta'].get('sources', []))} items")
- except json.JSONDecodeError:
- pass
- answer = "".join(events["content"])
- print(f"\n Answer length: {len(answer)} chars")
- return {
- "intent": events["intent"],
- "sources": events["meta"].get("sources", []) if events["meta"] else [],
- "answer_preview": answer[:500] + ("..." if len(answer) > 500 else ""),
- "status": events["status"],
- }
- # ============================================================
- # Main Diagnostic
- # ============================================================
- async def run_diagnosis(query: str, top_k: int = 20, call_api: bool = False,
- api_base: str = "https://pharmacopoeia.kailin.com.cn"):
- print("=" * 80)
- print(f" RAG Pipeline Diagnosis: '{query}'")
- print("=" * 80)
- # ===== Phase 1: Intent Classification =====
- print(f"\n{'='*80}")
- print(f" PHASE 1: Intent Classification")
- print(f"{'='*80}")
- intent = classify_intent(query)
- print(f" Query: '{query}'")
- print(f" Intent: '{intent}'")
- # Show why
- symptom_keywords = [
- "发烧", "咳嗽", "感冒", "腹泻", "头疼", "头痛",
- "嗓子疼", "流鼻涕", "鼻塞", "肚子疼", "胃疼",
- "过敏", "皮肤痒", "失眠", "便秘", "牙疼",
- ]
- matched = [kw for kw in symptom_keywords if kw in query]
- if matched:
- print(f" Matched symptom keywords: {matched}")
- print(f" >> This triggers 'symptom_advice' template -> LLM is told to recommend drugs")
- print(f"\n PROMPT TEMPLATE (intent={intent}):")
- # Inline prompt templates
- PROMPT_MAP = {
- "symptom_advice": "SYMPTOM_ADVICE: 病情评估 -> 用药方案(推荐药品) -> 注意事项 -> 就医指征",
- "drug_query": "DRUG_QUERY: 结论 -> 详细说明 -> 注意事项 -> 来源明细",
- "usage_guide": "USAGE_GUIDE: 结论 -> 用法用量 -> 禁忌 -> 不良反应 -> 注意事项",
- "regulation": "REGULATION: 摘要 -> 原文引用 -> 条款出处 -> 关联条款",
- "exam_tutor": "EXAM_TUTOR: 考点定位 -> 知识要点 -> 记忆技巧 -> 考试频率",
- "no_docs": "NO_DOCS: 结论 -> 详细说明(通用知识) -> 来源说明",
- }
- template = PROMPT_MAP.get(intent, "UNKNOWN")
- print(f" {template}")
- # ===== Phase 2: Database Statistics =====
- print(f"\n{'='*80}")
- print(f" PHASE 2: Database Overview")
- print(f"{'='*80}")
- try:
- stats = await get_db_stats()
- print(f" Total active drugs: {stats['total_drugs']}")
- print(f" Total chunks (with vec): {stats['total_chunks']}")
- print(f" Chunks containing '过敏': {stats['allergy_chunk_count']} "
- f"({stats['allergy_chunk_count']/max(stats['total_chunks'],1)*100:.1f}%)")
- print(f"\n Penicillin-related drugs in DB:")
- for name, cat, vol in stats['penicillin_drugs']:
- print(f" - {name} ({cat}, {vol})")
- print(f"\n Qingkailing-related drugs in DB:")
- for name, cat, vol in stats['qkl_drugs']:
- print(f" - {name} ({cat}, {vol})")
- print(f"\n Chunks by source (top 10):")
- for src, cnt in stats['by_source'][:10]:
- print(f" {src}: {cnt} chunks")
- print(f"\n Chunks by section (top 15):")
- for sec, cnt in stats['by_section'][:15]:
- print(f" {sec}: {cnt} chunks")
- except Exception as e:
- print(f" DB STATS FAILED: {e}")
- stats = None
- # ===== Phase 3: Vector Search =====
- print(f"\n{'='*80}")
- print(f" PHASE 3: Vector Search (pgvector cosine similarity, top-{top_k})")
- print(f"{'='*80}")
- try:
- print(f" Getting embedding for query...")
- query_vec = await get_query_embedding(query)
- print(f" Embedding dim: {len(query_vec)}")
- print(f" Searching drug_chunks...")
- docs = await vector_search(query_vec, top_k=top_k)
- print(f" Retrieved: {len(docs)} documents")
- if docs:
- print(f"\n Similarity score range: {docs[0]['score']:.4f} ~ {docs[-1]['score']:.4f}")
- above_threshold = sum(1 for d in docs if d['score'] >= 0.3)
- print(f" Above reranker threshold (0.3): {above_threshold}/{len(docs)}")
- # Show all results
- print(f"\n --- VECTOR SEARCH RESULTS ---")
- for i, doc in enumerate(docs):
- drug_name = extract_drug_name(doc['content'])
- content_preview = doc['content'][:120].replace('\n', ' ')
- flag = ""
- if drug_name and "青霉素" in drug_name:
- flag = " <<< PENICILLIN MATCH"
- elif drug_name and "清开灵" in drug_name:
- flag = " <<< QINGKAILING MATCH"
- print(f"\n [{i+1}] score={doc['score']:.4f} | drug='{drug_name}' | section='{doc['section']}'{flag}")
- print(f" {content_preview}...")
- except Exception as e:
- print(f" VECTOR SEARCH FAILED: {e}")
- import traceback
- traceback.print_exc()
- docs = []
- # ===== Phase 4: Reranker Simulation =====
- print(f"\n{'='*80}")
- print(f" PHASE 4: Reranker Simulation (keyword overlap + dedup)")
- print(f"{'='*80}")
- query_terms = tokenize_query(query)
- print(f" Query tokens ({len(query_terms)}): {sorted(query_terms)}")
- if docs:
- reranked = simulate_reranker(query, docs, top_k=5)
- print(f"\n After filtering (score>=0.3), keyword bonus, dedup: {len(reranked)} docs")
- print(f"\n --- RERANKED TOP-5 ---")
- for i, doc in enumerate(reranked):
- drug_name = extract_drug_name(doc['content'])
- content_preview = doc['content'][:150].replace('\n', ' ')
- print(f"\n [{i+1}] fused_score={doc['fused_score']:.4f} "
- f"(vec={doc['score']:.4f} + kw_bonus={doc['keyword_score']:.2f}*0.3)")
- print(f" drug='{drug_name}' | section='{doc['section']}' | source='{doc.get('source','')}'")
- print(f" {content_preview}")
- # Find Qingkailing results
- qkl_in_results = [d for d in docs if "清开灵" in d.get("content", "")]
- qkl_in_reranked = [d for d in reranked if "清开灵" in d.get("content", "")]
- print(f"\n Qingkailing in top-{top_k} vector results: {len(qkl_in_results)}")
- print(f" Qingkailing in top-5 reranked: {len(qkl_in_reranked)}")
- if qkl_in_reranked:
- print(f"\n *** QINGKAILING MADE IT TO TOP-5! ***")
- for d in qkl_in_reranked:
- print(f" drug='{extract_drug_name(d['content'])}' "
- f"score={d['score']:.4f} kw_bonus={d['keyword_score']:.2f}")
- # Show all Qingkailing in vector results
- if qkl_in_results:
- print(f"\n All Qingkailing entries in vector results:")
- for d in qkl_in_results:
- drug_name = extract_drug_name(d['content'])
- preview = d['content'][:100].replace('\n', ' ')
- print(f" [{drug_name}/{d['section']}] score={d['score']:.4f}: {preview}...")
- # ===== Phase 5: API Call (optional) =====
- if call_api:
- print(f"\n{'='*80}")
- print(f" PHASE 5: Production API Call")
- print(f"{'='*80}")
- try:
- api_result = await call_production_api(query, api_base)
- if api_result:
- print(f"\n API Intent: {api_result['intent']}")
- print(f" API Sources ({len(api_result['sources'])}):")
- for s in api_result['sources']:
- print(f" - {s.get('name', '?')} | section={s.get('section', '?')} "
- f"| score={s.get('score', 0):.4f} | source={s.get('source', '?')}")
- print(f"\n Answer preview:")
- print(f" {api_result['answer_preview']}")
- except Exception as e:
- print(f" API CALL FAILED: {e}")
- # ===== Phase 6: Summary =====
- print(f"\n{'='*80}")
- print(f" DIAGNOSIS SUMMARY")
- print(f"{'='*80}")
- issues = []
- # Issue 1: Intent
- if intent == "symptom_advice" and "过敏" in query:
- issues.append({
- "severity": "CRITICAL",
- "category": "Intent Classification",
- "detail": f"Query '{query}' classified as 'symptom_advice'. "
- f"The keyword '过敏' triggers symptom-based drug recommendation, "
- f"but this query is about managing a known drug allergy, not seeking "
- f"drugs for allergy symptoms.",
- "fix": "Add known-allergy detection: r'.+过敏(处理|怎么办|急救|替代)' -> usage_guide"
- })
- # Issue 2: Vector search data imbalance
- if stats:
- allergy_pct = stats['allergy_chunk_count'] / max(stats['total_chunks'], 1) * 100
- if allergy_pct > 30:
- issues.append({
- "severity": "HIGH",
- "category": "Data Imbalance",
- "detail": f"{allergy_pct:.0f}% of chunks contain '过敏'. "
- f"Any allergy-related query will match a huge number of "
- f"unrelated drug entries.",
- "fix": "Consider section-specific indexing or adding drug-name boost "
- "in vector search scoring."
- })
- if stats['qkl_drugs'] and not stats['penicillin_drugs']:
- issues.append({
- "severity": "HIGH",
- "category": "Missing Data",
- "detail": f"Qingkailing entries found ({len(stats['qkl_drugs'])}), "
- f"but no penicillin entries in database. "
- f"System cannot return relevant results for penicillin queries.",
- "fix": "Ensure chemical drug data (volume 2) is imported."
- })
- # Issue 3: Reranker weakness
- if docs:
- top_scores = [d['score'] for d in docs[:5]]
- score_spread = top_scores[0] - top_scores[-1] if len(top_scores) > 1 else 0
- if score_spread < 0.1:
- issues.append({
- "severity": "MEDIUM",
- "category": "Weak Reranker",
- "detail": f"Top-5 vector scores too close (spread={score_spread:.4f}). "
- f"Keyword-based reranker cannot effectively distinguish relevance.",
- "fix": "Implement Cross-Encoder reranker (BGE-Reranker-v2-m3)."
- })
- # Issue 4: Always present
- issues.append({
- "severity": "MEDIUM",
- "category": "Prompt Template",
- "detail": "SYMPTOM_ADVICE template forces LLM to recommend drugs "
- "('用药方案: 方案一, 方案二...'). When query is about "
- "allergy management, this produces incorrect responses.",
- "fix": "Add constraint in SYMPTOM_ADVICE: if user asks about known drug "
- "allergy, provide management advice, not drug recommendations."
- })
- for issue in issues:
- print(f"\n [{issue['severity']}] {issue['category']}")
- print(f" {issue['detail']}")
- print(f" Fix: {issue['fix']}")
- print(f"\n{'='*80}")
- print(f" Diagnosis complete. {len(issues)} issues found.")
- print(f"{'='*80}")
- # ============================================================
- # CLI Entry Point
- # ============================================================
- if __name__ == "__main__":
- parser = argparse.ArgumentParser(
- description="Production RAG pipeline diagnostics"
- )
- parser.add_argument(
- "query", nargs="?", default="青霉素过敏处理",
- help="Query to diagnose (default: 青霉素过敏处理)"
- )
- parser.add_argument(
- "--top-k", type=int, default=20,
- help="Number of vector search results to retrieve (default: 20)"
- )
- parser.add_argument(
- "--call-api", action="store_true",
- help="Also call the production API endpoint"
- )
- parser.add_argument(
- "--api-base", default="https://pharmacopoeia.kailin.com.cn",
- help="API base URL (default: https://pharmacopoeia.kailin.com.cn)"
- )
- args = parser.parse_args()
- asyncio.run(run_diagnosis(
- query=args.query,
- top_k=args.top_k,
- call_api=args.call_api,
- api_base=args.api_base,
- ))
|