wiki_compile.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. #!/usr/bin/env python3
  2. """
  3. Wiki Full Compilation: Read domain docs → LLM understand → Write wiki pages
  4. Uses vLLM on NPU for fast inference
  5. For each document:
  6. 1. Read extracted text
  7. 2. LLM summarizes → write sources/<filename>.md
  8. 3. LLM extracts entities → create/update entities/<entity>.md
  9. 4. LLM identifies topics → create/update topics/<topic>.md
  10. 5. Update index.md and log.md
  11. Supports resume from interruption.
  12. """
  13. import json
  14. import os
  15. import re
  16. import sys
  17. import time
  18. import urllib.request
  19. from datetime import datetime
  20. from pathlib import Path
  21. from config import cfg
  22. PROCESSED_DIR = cfg.processed_dir
  23. WIKI_DIR = cfg.wiki_dir
  24. PROGRESS_FILE = WIKI_DIR / '.compile_progress.json'
  25. # LLM API endpoints — read from env vars so the job runner can override them.
  26. # Priority: VLLM_URL env (set by knowledge.py job runner) → Ollama default → vLLM fallback
  27. # NOTE: never default to port 8000 — that's the AgentPaaS FastAPI server,
  28. # which returns index.html (SPA catch-all) with HTTP 200, fooling detect_api().
  29. VLLM_URL = os.environ.get('VLLM_URL', 'http://127.0.0.1:8001/v1/chat/completions')
  30. OLLAMA_URL = os.environ.get('OLLAMA_URL', 'http://127.0.0.1:11434/v1/chat/completions')
  31. API_URL = None # auto-detect
  32. def _is_llm_endpoint(url: str, timeout: int = 10) -> bool:
  33. """Return True only if `url` responds like an OpenAI-compatible LLM.
  34. We send a minimal chat request and verify the response contains 'choices'
  35. or a known Ollama/vLLM error (model not found etc.) — NOT HTML.
  36. """
  37. try:
  38. body = json.dumps({
  39. "model": os.environ.get('LLM_MODEL', 'qwen2.5:7b'),
  40. "messages": [{"role": "user", "content": "hi"}],
  41. "max_tokens": 3,
  42. }).encode('utf-8')
  43. req = urllib.request.Request(
  44. url, data=body,
  45. headers={"Content-Type": "application/json"}, method="POST"
  46. )
  47. with urllib.request.urlopen(req, timeout=timeout) as resp:
  48. raw = resp.read()
  49. # Must be valid JSON and look like an LLM response (not HTML)
  50. if not raw or raw[:1] in (b'<', b'!'):
  51. return False
  52. data = json.loads(raw)
  53. # OpenAI format: {"choices": [...]} or Ollama error: {"error": "..."}
  54. return "choices" in data or "error" in data or "message" in data
  55. except urllib.error.HTTPError as e:
  56. # 4xx from a real LLM (model not loaded, bad request) — server IS there
  57. if e.code in (400, 404, 422, 503):
  58. return True
  59. return False
  60. except Exception:
  61. return False
  62. def detect_api():
  63. global API_URL
  64. candidates = list(dict.fromkeys([VLLM_URL, OLLAMA_URL])) # dedup, preserve order
  65. for url in candidates:
  66. if _is_llm_endpoint(url):
  67. API_URL = url
  68. label = "Ollama" if "11434" in url or "11435" in url else "vLLM/API"
  69. print(f"Using: {label} ({url})")
  70. return
  71. print("ERROR: No LLM API available!")
  72. print(f" Tried: {candidates}")
  73. print(" → Please start Ollama: brew install ollama && ollama serve && ollama pull qwen2.5:7b")
  74. sys.exit(1)
  75. def llm_chat(prompt, max_tokens=2048, temperature=0.2):
  76. model = os.environ.get('LLM_MODEL', 'qwen2.5:7b')
  77. body = json.dumps({
  78. "model": model,
  79. "messages": [{"role": "user", "content": prompt}],
  80. "temperature": temperature,
  81. "max_tokens": max_tokens,
  82. }, ensure_ascii=False).encode('utf-8')
  83. req = urllib.request.Request(API_URL, data=body,
  84. headers={"Content-Type": "application/json"}, method="POST")
  85. with urllib.request.urlopen(req, timeout=300) as resp:
  86. data = json.loads(resp.read())
  87. return data["choices"][0]["message"]["content"].strip()
  88. def safe_filename(name, max_len=80):
  89. """Convert to safe filename"""
  90. name = re.sub(r'[\\/:*?"<>|]', '_', name)
  91. name = re.sub(r'\s+', '_', name)
  92. return name[:max_len]
  93. def load_progress():
  94. if PROGRESS_FILE.exists():
  95. with open(PROGRESS_FILE) as f:
  96. return json.load(f)
  97. return {'done': [], 'entities': {}, 'topics': {}, 'relations': []}
  98. def save_progress(progress):
  99. with open(PROGRESS_FILE, 'w', encoding='utf-8') as f:
  100. json.dump(progress, f, ensure_ascii=False, indent=2)
  101. def _extract_date_from_name(doc_name):
  102. """从文件名中提取日期信息"""
  103. # 匹配 "2017年8月31日起施行" 或 "2022年4月1日"
  104. m = re.search(r'(\d{4})年(\d{1,2})月(\d{1,2})日', doc_name)
  105. if m:
  106. return f"{m.group(1)}年{m.group(2)}月{m.group(3)}日"
  107. # 匹配 "2017年"
  108. m = re.search(r'(\d{4})年', doc_name)
  109. if m:
  110. return f"{m.group(1)}年"
  111. return "未知"
  112. def compile_source(doc_name, text, progress):
  113. """Step 1: Create source summary page"""
  114. # Truncate long documents
  115. text_sample = text[:6000] if len(text) > 6000 else text
  116. # Extract date from filename for temporal normalization
  117. doc_date = _extract_date_from_name(doc_name)
  118. prompt = f"""请阅读以下海事领域文档,输出结构化摘要。
  119. 文档名: {doc_name}
  120. 文档日期: {doc_date}
  121. 文档内容:
  122. {text_sample}
  123. 请按以下格式输出(不要输出其他内容):
  124. ## 摘要
  125. [2-4句话概括文档核心内容,必须包含具体的施行日期和文号]
  126. ## 关键信息
  127. - [要点1]
  128. - [要点2]
  129. - [要点3]
  130. (最多8个要点,每个要点尽量包含具体条款号、数字、日期)
  131. ## 时间线
  132. [列出文档中提到的所有时间节点,格式: 绝对日期|事件]
  133. {doc_date}|本文件施行
  134. (**重要**: 文档中的所有相对时间必须转换为绝对时间:
  135. - "自发布之日起施行" → "{doc_date}|施行"
  136. - "近年来" → 以{doc_date}为基准,推算为具体年份范围
  137. - "去年" → 以{doc_date}为基准,推算具体年份
  138. - "本条例修订前" → 写明具体的修订前版本年份
  139. - "三年内" → 从{doc_date}起算,写明截止的具体年份
  140. 禁止使用任何相对时间表述,全部转为绝对年月日)
  141. ## 实体列表
  142. [列出文档中涉及的重要实体,每行一个,格式: 类型|名称|说明]
  143. 法规|XXX法|(含施行日期和文号)
  144. 机构|XXX局|...
  145. 概念|XXX制度|...
  146. ## 实体关系
  147. [列出文档中实体之间的关系,每行一个,格式: 主体|关系|客体|说明]
  148. 交通运输部|制定|船员管理规定|交通运输部负责制定船员管理相关规定
  149. 海事局|隶属|交通运输部|海事局是交通运输部下属机构
  150. 船员适任证书|依据|STCW公约|船员适任证书制度依据STCW国际公约
  151. (列出文档中所有重要的实体间关系,包括:上下级/隶属、制定/发布、依据/引用、适用/管辖、修订/废止、时间先后等)
  152. ## 相关主题
  153. [列出文档涉及的主题领域,每行一个]
  154. 船员管理
  155. 船舶安全
  156. ...
  157. ## 元数据
  158. - 发布机构: [如有]
  159. - 施行日期: {doc_date}
  160. - 文号: [如有]
  161. - 法律层级: [全国人大/国务院/部委/海事局/其他]
  162. - 是否现行有效: [是/否/已被修订]
  163. - 修订历史: [如有,列出历次修订的绝对日期]"""
  164. result = llm_chat(prompt)
  165. # Write source page
  166. source_path = WIKI_DIR / 'sources' / f'{safe_filename(doc_name)}.md'
  167. source_path.parent.mkdir(parents=True, exist_ok=True)
  168. with open(source_path, 'w', encoding='utf-8') as f:
  169. f.write(f'# {doc_name}\n\n')
  170. f.write(f'[来源: {doc_name}]\n\n')
  171. f.write(result)
  172. f.write('\n')
  173. return result
  174. def extract_entities(llm_result, doc_name, progress):
  175. """Step 2: Extract and update entity pages"""
  176. entities_found = []
  177. lines = llm_result.split('\n')
  178. in_entity_section = False
  179. for line in lines:
  180. if '## 实体列表' in line:
  181. in_entity_section = True
  182. continue
  183. if line.startswith('## ') and in_entity_section:
  184. in_entity_section = False
  185. continue
  186. if in_entity_section and '|' in line:
  187. parts = [p.strip() for p in line.strip('- ').split('|')]
  188. if len(parts) >= 2:
  189. etype = parts[0]
  190. ename = parts[1]
  191. edesc = parts[2] if len(parts) > 2 else ''
  192. if ename and len(ename) > 1:
  193. entities_found.append((etype, ename, edesc))
  194. # Update entity pages
  195. for etype, ename, edesc in entities_found[:10]: # Max 10 per doc
  196. entity_file = safe_filename(ename)
  197. entity_path = WIKI_DIR / 'entities' / f'{entity_file}.md'
  198. entity_path.parent.mkdir(parents=True, exist_ok=True)
  199. if entity_path.exists():
  200. # Append reference
  201. existing = entity_path.read_text(encoding='utf-8')
  202. if doc_name not in existing:
  203. with open(entity_path, 'a', encoding='utf-8') as f:
  204. f.write(f'\n- {edesc} [来源: {doc_name}]\n')
  205. else:
  206. # Create new entity page
  207. with open(entity_path, 'w', encoding='utf-8') as f:
  208. f.write(f'# {ename}\n\n')
  209. f.write(f'**类型**: {etype}\n\n')
  210. f.write(f'## 描述\n\n{edesc}\n\n')
  211. f.write(f'## 相关文档\n\n')
  212. f.write(f'- {doc_name}\n')
  213. # Track in progress
  214. if ename not in progress['entities']:
  215. progress['entities'][ename] = {'type': etype, 'refs': []}
  216. if doc_name not in progress['entities'][ename]['refs']:
  217. progress['entities'][ename]['refs'].append(doc_name)
  218. return entities_found
  219. def extract_topics(llm_result, doc_name, progress):
  220. """Step 3: Extract and update topic pages"""
  221. topics_found = []
  222. lines = llm_result.split('\n')
  223. in_topic_section = False
  224. for line in lines:
  225. if '## 相关主题' in line:
  226. in_topic_section = True
  227. continue
  228. if line.startswith('## ') and in_topic_section:
  229. in_topic_section = False
  230. continue
  231. if in_topic_section:
  232. topic = line.strip('- ').strip()
  233. if topic and len(topic) > 1 and not topic.startswith('#'):
  234. topics_found.append(topic)
  235. # Update topic pages
  236. for topic in topics_found[:5]: # Max 5 per doc
  237. topic_file = safe_filename(topic)
  238. topic_path = WIKI_DIR / 'topics' / f'{topic_file}.md'
  239. topic_path.parent.mkdir(parents=True, exist_ok=True)
  240. if topic_path.exists():
  241. existing = topic_path.read_text(encoding='utf-8')
  242. if doc_name not in existing:
  243. with open(topic_path, 'a', encoding='utf-8') as f:
  244. f.write(f'- [[{safe_filename(doc_name)}]] - {doc_name}\n')
  245. else:
  246. with open(topic_path, 'w', encoding='utf-8') as f:
  247. f.write(f'# {topic}\n\n')
  248. f.write(f'## 相关文档\n\n')
  249. f.write(f'- [[{safe_filename(doc_name)}]] - {doc_name}\n')
  250. if topic not in progress['topics']:
  251. progress['topics'][topic] = []
  252. if doc_name not in progress['topics'][topic]:
  253. progress['topics'][topic].append(doc_name)
  254. return topics_found
  255. def extract_relations(llm_result, doc_name, progress):
  256. """Step 4: Extract entity relations and build knowledge graph"""
  257. relations_found = []
  258. lines = llm_result.split('\n')
  259. in_relation_section = False
  260. for line in lines:
  261. if '## 实体关系' in line:
  262. in_relation_section = True
  263. continue
  264. if line.startswith('## ') and in_relation_section:
  265. in_relation_section = False
  266. continue
  267. if in_relation_section and '|' in line:
  268. parts = [p.strip() for p in line.strip('- (').split('|')]
  269. if len(parts) >= 3:
  270. subject = parts[0]
  271. relation = parts[1]
  272. obj = parts[2]
  273. desc = parts[3] if len(parts) > 3 else ''
  274. if subject and relation and obj and len(subject) > 1:
  275. relations_found.append({
  276. 'subject': subject,
  277. 'relation': relation,
  278. 'object': obj,
  279. 'description': desc,
  280. 'source': doc_name,
  281. })
  282. # Store relations in progress
  283. if 'relations' not in progress:
  284. progress['relations'] = []
  285. for rel in relations_found[:15]: # Max 15 per doc
  286. # Deduplicate
  287. existing = any(
  288. r['subject'] == rel['subject'] and
  289. r['relation'] == rel['relation'] and
  290. r['object'] == rel['object']
  291. for r in progress['relations']
  292. )
  293. if not existing:
  294. progress['relations'].append(rel)
  295. # Update entity pages with relations
  296. for rel in relations_found[:15]:
  297. for ename in [rel['subject'], rel['object']]:
  298. entity_file = safe_filename(ename)
  299. entity_path = WIKI_DIR / 'entities' / f'{entity_file}.md'
  300. if entity_path.exists():
  301. existing = entity_path.read_text(encoding='utf-8')
  302. if '## 关系' not in existing:
  303. with open(entity_path, 'a', encoding='utf-8') as f:
  304. f.write(f'\n## 关系\n\n')
  305. rel_line = f'- {rel["subject"]} **{rel["relation"]}** {rel["object"]}'
  306. if rel['description']:
  307. rel_line += f' — {rel["description"]}'
  308. rel_line += f' [来源: {doc_name}]\n'
  309. existing = entity_path.read_text(encoding='utf-8')
  310. if rel_line.strip() not in existing:
  311. with open(entity_path, 'a', encoding='utf-8') as f:
  312. f.write(rel_line)
  313. # Write/update relations graph file
  314. relations_path = WIKI_DIR / 'relations.json'
  315. with open(relations_path, 'w', encoding='utf-8') as f:
  316. json.dump(progress['relations'], f, ensure_ascii=False, indent=2)
  317. return relations_found
  318. def update_index(progress):
  319. """Update wiki index.md"""
  320. sources = sorted(WIKI_DIR.glob('sources/*.md'))
  321. entities = sorted(WIKI_DIR.glob('entities/*.md'))
  322. topics = sorted(WIKI_DIR.glob('topics/*.md'))
  323. analyses = sorted(WIKI_DIR.glob('analyses/*.md'))
  324. index = f"""# Maritime Wiki Index
  325. > Auto-compiled from domain documents by wikiagent
  326. > Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  327. ## Sources ({len(sources)} files)
  328. """
  329. for s in sources[:50]: # Show first 50
  330. index += f'- [[{s.stem}]]\n'
  331. if len(sources) > 50:
  332. index += f'\n... and {len(sources) - 50} more\n'
  333. index += f'\n## Entities ({len(entities)})\n\n'
  334. # Group by type
  335. entity_types = {}
  336. for ename, einfo in progress.get('entities', {}).items():
  337. etype = einfo.get('type', 'other')
  338. if etype not in entity_types:
  339. entity_types[etype] = []
  340. entity_types[etype].append((ename, len(einfo.get('refs', []))))
  341. for etype, elist in sorted(entity_types.items()):
  342. index += f'### {etype} ({len(elist)})\n'
  343. for ename, ref_count in sorted(elist, key=lambda x: -x[1])[:20]:
  344. index += f'- [[{safe_filename(ename)}]] ({ref_count} refs)\n'
  345. if len(elist) > 20:
  346. index += f'- ... and {len(elist) - 20} more\n'
  347. index += '\n'
  348. index += f'## Topics ({len(topics)})\n\n'
  349. for tname, trefs in sorted(progress.get('topics', {}).items(), key=lambda x: -len(x[1]))[:30]:
  350. index += f'- [[{safe_filename(tname)}]] ({len(trefs)} docs)\n'
  351. if len(topics) > 30:
  352. index += f'\n... and {len(topics) - 30} more\n'
  353. index += f'\n## Analyses ({len(analyses)})\n\n'
  354. for a in analyses[:20]:
  355. index += f'- [[{a.stem}]]\n'
  356. # Relations summary
  357. relations = progress.get('relations', [])
  358. if relations:
  359. index += f'\n## Relations ({len(relations)})\n\n'
  360. # Group by relation type
  361. rel_types = {}
  362. for r in relations:
  363. rt = r['relation']
  364. if rt not in rel_types:
  365. rel_types[rt] = []
  366. rel_types[rt].append(r)
  367. for rt, rels in sorted(rel_types.items(), key=lambda x: -len(x[1]))[:15]:
  368. index += f'### {rt} ({len(rels)})\n'
  369. for r in rels[:5]:
  370. index += f'- {r["subject"]} → {r["object"]}\n'
  371. if len(rels) > 5:
  372. index += f'- ... and {len(rels) - 5} more\n'
  373. index += '\n'
  374. index += f'\n---\nStats: {len(sources)} sources, {len(entities)} entities, {len(topics)} topics, {len(relations)} relations, {len(analyses)} analyses\n'
  375. with open(WIKI_DIR / 'index.md', 'w', encoding='utf-8') as f:
  376. f.write(index)
  377. def update_log(doc_name, entities_count, topics_count, relations_count=0):
  378. """Append to log.md"""
  379. log_path = WIKI_DIR / 'log.md'
  380. timestamp = datetime.now().strftime('%Y-%m-%d %H:%M')
  381. entry = f'## [{timestamp}] Ingest | {doc_name}\n- Entities: {entities_count}, Topics: {topics_count}, Relations: {relations_count}\n\n'
  382. if log_path.exists():
  383. existing = log_path.read_text(encoding='utf-8')
  384. # Insert after header
  385. if '_no records_' in existing or '_no operations_' in existing:
  386. existing = '# Operation Log\n\n'
  387. with open(log_path, 'w', encoding='utf-8') as f:
  388. f.write(existing + entry)
  389. else:
  390. with open(log_path, 'w', encoding='utf-8') as f:
  391. f.write('# Operation Log\n\n' + entry)
  392. def main():
  393. detect_api()
  394. # Ensure wiki dirs exist
  395. for d in ['sources', 'entities', 'topics', 'analyses']:
  396. (WIKI_DIR / d).mkdir(parents=True, exist_ok=True)
  397. # WIKI_REBUILD=1 → clear progress file and all generated pages for a clean rebuild
  398. if os.environ.get('WIKI_REBUILD') == '1':
  399. if PROGRESS_FILE.exists():
  400. PROGRESS_FILE.unlink()
  401. print("[Rebuild] 已清除进度文件")
  402. for d in ['sources', 'entities', 'topics']:
  403. for f in (WIKI_DIR / d).glob('*.md'):
  404. f.unlink()
  405. print("[Rebuild] 已清除旧 wiki 页面,开始全量重建")
  406. # Load progress
  407. progress = load_progress()
  408. # Separate truly-done from errored: only skip docs with generated source pages
  409. compiled_pages = {f.stem for f in (WIKI_DIR / 'sources').glob('*.md')}
  410. done_set = compiled_pages # use actual output files as ground truth, not the progress file
  411. progress['done'] = [d for d in progress['done'] if d in compiled_pages]
  412. # Get all text files
  413. all_docs = sorted(PROCESSED_DIR.glob('*.txt'))
  414. remaining = [d for d in all_docs if d.stem not in done_set]
  415. total = len(all_docs)
  416. print(f"Total documents: {total}")
  417. print(f"Already compiled: {len(done_set)}")
  418. print(f"Remaining: {len(remaining)}")
  419. print("=" * 60)
  420. start_time = time.time()
  421. errors = 0
  422. for idx, doc_path in enumerate(remaining):
  423. doc_name = doc_path.stem
  424. qnum = len(done_set) + idx + 1
  425. text = doc_path.read_text(encoding='utf-8')
  426. if len(text) < 50:
  427. print(f"[{qnum}/{total}] SKIP (too short): {doc_name[:60]}")
  428. progress['done'].append(doc_name)
  429. continue
  430. print(f"\n[{qnum}/{total}] {doc_name[:70]}...")
  431. try:
  432. # Step 1: Compile source summary
  433. t0 = time.time()
  434. llm_result = compile_source(doc_name, text, progress)
  435. t1 = time.time()
  436. print(f" Source page: {t1-t0:.1f}s")
  437. # Step 2: Extract entities
  438. entities = extract_entities(llm_result, doc_name, progress)
  439. print(f" Entities: {len(entities)} found")
  440. # Step 3: Extract topics
  441. topics = extract_topics(llm_result, doc_name, progress)
  442. print(f" Topics: {len(topics)} found")
  443. # Step 4: Extract relations
  444. relations = extract_relations(llm_result, doc_name, progress)
  445. print(f" Relations: {len(relations)} found")
  446. # Step 5: Update log
  447. update_log(doc_name, len(entities), len(topics), len(relations))
  448. progress['done'].append(doc_name)
  449. except Exception as e:
  450. errors += 1
  451. print(f" ERROR: {e}")
  452. # Do NOT add to done — failed docs will be retried on next incremental run
  453. # (only successfully compiled docs with a source page count as done)
  454. # Save progress and update index every 20 docs
  455. if (idx + 1) % 20 == 0 or idx == len(remaining) - 1:
  456. save_progress(progress)
  457. update_index(progress)
  458. elapsed = time.time() - start_time
  459. avg = elapsed / (idx + 1)
  460. eta = avg * (len(remaining) - idx - 1)
  461. print(f"\n [Progress] {qnum}/{total} done | "
  462. f"Elapsed: {elapsed/60:.1f}min | ETA: {eta/60:.1f}min | "
  463. f"Errors: {errors} | "
  464. f"Entities: {len(progress['entities'])} | "
  465. f"Topics: {len(progress['topics'])} | "
  466. f"Relations: {len(progress.get('relations', []))}")
  467. # Final update
  468. save_progress(progress)
  469. update_index(progress)
  470. total_time = time.time() - start_time
  471. sources = len(list(WIKI_DIR.glob('sources/*.md')))
  472. entities = len(list(WIKI_DIR.glob('entities/*.md')))
  473. topics = len(list(WIKI_DIR.glob('topics/*.md')))
  474. print(f"\n{'=' * 60}")
  475. print(f"Wiki Compilation Complete!")
  476. print(f" Time: {total_time/60:.1f} minutes")
  477. print(f" Sources: {sources}")
  478. print(f" Entities: {entities}")
  479. print(f" Topics: {topics}")
  480. print(f" Errors: {errors}")
  481. print(f" Wiki dir: {WIKI_DIR}")
  482. if __name__ == '__main__':
  483. main()