#!/usr/bin/env python3 """ topic_guide_generator.py — XKB 領域導覽生成器 針對某個主題(tag 或 category),從現有 cards 自動生成: - 領域基礎術語與 False Friends - 建議閱讀順序(從基礎到進階) - 主要作者與關鍵論文 - 目前的知識共識 vs 缺口 Usage: python3 topic_guide_generator.py --topic "醫療AI影像診斷" python3 topic_guide_generator.py --category research python3 topic_guide_generator.py --tag "影像診斷" --tag "人工智慧" python3 topic_guide_generator.py --topic "醫療AI" --out wiki/topics/medical-ai-guide.md """ from __future__ import annotations import argparse import json import os import re import sys import urllib.request from datetime import datetime, timezone from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import xkb_paths sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "tools")) from runtime_config import runtime_env WORKSPACE = xkb_paths.WORKSPACE _SKILL_DIR = Path(__file__).resolve().parent.parent INDEX_PATH = WORKSPACE / "memory" / "bookmarks" / "search_index.json" WIKI_DIR = xkb_paths.WIKI_DIR / "topics" GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1/models/gemini-2.5-flash:generateContent" def _get_gemini_key() -> str: return runtime_env().get("GEMINI_API_KEY", "") def _call_gemini(key: str, prompt: str, retries: int = 3) -> str: import time payload = json.dumps({"contents": [{"parts": [{"text": prompt}]}]}).encode() url = f"{GEMINI_API_URL}?key={key}" for attempt in range(retries): try: req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"}) with urllib.request.urlopen(req, timeout=90) as r: data = json.loads(r.read()) return data["candidates"][0]["content"]["parts"][0]["text"].strip() except Exception as e: if attempt < retries - 1: wait = (attempt + 1) * 5 print(f" retrying in {wait}s... ({e})") time.sleep(wait) else: raise def load_cards(category: str | None, tags: list[str]) -> list[dict]: """Load relevant cards from the search index.""" data = json.loads(INDEX_PATH.read_text(encoding="utf-8")) items = data.get("items", []) results = [] for item in items: if category and item.get("category") != category: continue if tags: item_tags = [t.lower() for t in (item.get("tags") or [])] item_summary = (item.get("summary") or "").lower() if not any(t.lower() in " ".join(item_tags) + " " + item_summary for t in tags): continue results.append(item) return results def build_card_digest(cards: list[dict]) -> str: """Build a condensed digest of cards for the LLM.""" cards = cards[:15] # Cap at 15 to keep prompt manageable parts = [] for i, card in enumerate(cards, 1): title = card.get("title", "")[:100] summary = card.get("summary", "")[:200] card_tags = ", ".join((card.get("tags") or [])[:6]) author = card.get("author", "")[:60] parts.append( f"[{i}] {title}\n" f" Authors: {author}\n" f" Tags: {card_tags}\n" f" Summary: {summary}" ) return "\n\n".join(parts) def generate_topic_guide(topic: str, cards: list[dict], key: str) -> str: """Call Gemini to generate a structured topic guide.""" digest = build_card_digest(cards) prompt = f"""You are a knowledge curator. Based on the following {len(cards)} knowledge cards about "{topic}", generate a comprehensive "Topic Guide" for someone new to this field. The cards are: {digest} Generate the topic guide in Traditional Chinese with this exact structure: # 領域導覽:{topic} > 基於 {len(cards)} 篇相關文獻自動生成 | {datetime.now().strftime('%Y-%m-%d')} ## 一、這個領域在解決什麼問題? (2-3 句話說明核心問題意識,為什麼這個領域值得研究) ## 二、必知基礎術語 列出 5-8 個關鍵術語,每個包含: - **術語**:正確含義 + 常見誤解(如有) ## 三、建議閱讀順序 根據概念難度和依賴關係,列出建議的閱讀路徑: ### 第一步:建立基礎概念 - [論文編號] 標題 ——理由:為什麼先讀這篇? ### 第二步:深入方法論 - [論文編號] 標題 ——理由 ### 第三步:進階應用與挑戰 - [論文編號] 標題 ——理由 (如有需要可增加步驟,每步驟1-3篇) ## 四、目前的知識共識 這個領域中,哪些觀點已有較多證據支持?(3-5點) ## 五、知識缺口與爭議 根據現有資料,哪些問題仍未解決或有爭議?(3-5點) ## 六、主要研究者與機構 列出收錄文獻中出現的重要作者或機構 ## 七、延伸學習建議 根據目前收錄資料的缺口,建議下一步應該補充哪些方向的文獻? --- *此導覽由 XKB 自動生成,基於已收錄的知識卡片。如需更新,重新執行 topic_guide_generator.py*""" return _call_gemini(key, prompt) def main() -> int: parser = argparse.ArgumentParser(description="XKB 領域導覽生成器") parser.add_argument("--topic", help="主題名稱(用於標題和 LLM prompt)") parser.add_argument("--category", help="按 category 篩選 cards") parser.add_argument("--tag", action="append", dest="tags", default=[], help="按 tag 篩選(可重複)") parser.add_argument("--out", help="輸出檔案路徑(預設自動生成)") parser.add_argument("--min-cards", type=int, default=3, help="最少需要幾張 card 才生成(預設 3)") parser.add_argument("--dry-run", action="store_true", help="只顯示符合的 cards,不呼叫 LLM") args = parser.parse_args() if not args.topic and not args.category and not args.tags: print("Error: 需要至少一個篩選條件:--topic, --category, 或 --tag") return 1 topic = args.topic or args.category or "+".join(args.tags) print(f"📚 篩選 cards:topic={topic}, category={args.category}, tags={args.tags}") cards = load_cards(args.category, args.tags) if not cards: print("❌ 沒有找到符合條件的 cards") return 1 print(f" 找到 {len(cards)} 張 cards") for card in cards: print(f" - {card.get('title','')[:70]}") if len(cards) < args.min_cards: print(f"⚠ cards 數量不足 {args.min_cards},跳過生成(用 --min-cards 1 強制執行)") return 0 if args.dry_run: print("\n[dry-run] 不呼叫 LLM") return 0 key = _get_gemini_key() if not key: print("❌ GEMINI_API_KEY not found") return 1 print(f"\n🤖 呼叫 Gemini 生成領域導覽({len(cards)} 張 cards)...") guide = generate_topic_guide(topic, cards, key) # Determine output path if args.out: out_path = WORKSPACE / args.out if not Path(args.out).is_absolute() else Path(args.out) else: slug = re.sub(r"[^\w\-]", "-", topic.lower())[:40].strip("-") out_path = WIKI_DIR / f"{slug}-guide.md" out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(guide, encoding="utf-8") print(f"\n✅ 領域導覽已儲存:{out_path}") print(f"\n--- 預覽(前 500 字)---") print(guide[:500]) return 0 import xkb_usage # noqa: E402 — 量測誰在跑,見 scripts/xkb_usage.py if __name__ == "__main__": xkb_usage.record(__file__) raise SystemExit(main())