| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150 |
- #!/usr/bin/env python3
- """
- Maritime QA Comparison Demo
- RAG vs Wiki side-by-side comparison
- With server-side conversation history persistence
- """
- import json
- import os
- import sys
- import time
- import threading
- from datetime import datetime
- from flask import Flask, request, jsonify, render_template_string
- import yaml
- # Load paths from agent config
- _agent_cfg_path = os.environ.get('AGENT_CONFIG', '/app/agentexample/qaagent67/agent-config.yml')
- _wiki_cfg_path = os.environ.get('WIKI_CONFIG', '/app/agentexample/qaagent67wiki/agent-config.yml')
- _cfg = {}
- _wiki_cfg = {}
- if os.path.exists(_agent_cfg_path):
- with open(_agent_cfg_path) as f:
- _cfg = yaml.safe_load(f)
- if os.path.exists(_wiki_cfg_path):
- with open(_wiki_cfg_path) as f:
- _wiki_cfg = yaml.safe_load(f)
- # ═══ instance.yml overlay (domain-specific overrides) ═══
- def _deep_merge(base, override):
- import copy
- result = copy.deepcopy(base)
- for k, v in (override or {}).items():
- if k in result and isinstance(result[k], dict) and isinstance(v, dict):
- result[k] = _deep_merge(result[k], v)
- else:
- result[k] = copy.deepcopy(v)
- return result
- _instance_cfg_path = os.environ.get('INSTANCE_CONFIG', '')
- if not _instance_cfg_path:
- _base = (_cfg.get('knowledge', {}) or {}).get('baseDir', '')
- if _base:
- _cand = os.path.join(_base, 'instance.yml')
- if os.path.isfile(_cand):
- _instance_cfg_path = _cand
- if _instance_cfg_path and os.path.isfile(_instance_cfg_path):
- with open(_instance_cfg_path) as _f:
- _instance = yaml.safe_load(_f) or {}
- _cfg = _deep_merge(_cfg, _instance)
- print('[config] loaded instance overlay:', _instance_cfg_path)
- _knowledge = _cfg.get('knowledge', {})
- KNOWLEDGE_BASE = _knowledge.get('baseDir', os.environ.get('KNOWLEDGE_DIR', '/data/knowledge/maritime'))
- sys.path.insert(0, KNOWLEDGE_BASE)
- try:
- from search_unified import search
- except ImportError:
- from search_engine import search
- from lambdagent.providers import create_provider
- app = Flask(__name__)
- HISTORY_FILE = os.environ.get('HISTORY_FILE', '/app/qademo/conversation_history.json')
- # WIKI_DIR — prefer instance.yml overlay (_cfg.wiki.dir), then standalone wiki config, then default
- WIKI_DIR = (
- (_cfg.get('wiki', {}) or {}).get('dir', '')
- or (_wiki_cfg.get('wiki', {}) or {}).get('dir', '')
- or '/app/agentexample/qaagent67wiki/wiki'
- )
- # ═══ Demo UI config (domain-specific, loaded from instance.yml) ═══
- _demo = _cfg.get('demo', {}) or {}
- DEMO_TITLE = _demo.get('title', 'Knowledge QA Demo')
- DEMO_TAGLINE = _demo.get('tagline', 'RAG vs Wiki | Qwen2.5:32B local')
- DEMO_PLACEHOLDER = _demo.get('placeholder', 'Please enter a question...')
- DEMO_DOMAIN = _demo.get('domain', 'knowledge')
- DEMO_EXAMPLES = _demo.get('examples', []) # list of {q, label}
- DEMO_PORT = int(os.environ.get('PORT') or _demo.get('port', 8080))
- DEMO_BIND = os.environ.get('BIND_HOST') or _demo.get('bind') or '0.0.0.0'
- # 左右面板默认选项: 海事域左边默认 LambdaRAG,其他域默认 Lite
- _is_maritime = 'maritime' in DEMO_DOMAIN.lower() or '海事' in DEMO_TITLE
- DEMO_LEFT_DEFAULT = _demo.get('leftDefault', 'rag' if _is_maritime else 'lite')
- DEMO_RIGHT_DEFAULT = _demo.get('rightDefault', 'wiki')
- def _html_attr_esc(s):
- """Escape for HTML attribute in double quotes."""
- return (str(s).replace('&', '&').replace('<', '<').replace('>', '>')
- .replace('"', '"').replace("'", '''))
- def _html_text_esc(s):
- return (str(s).replace('&', '&').replace('<', '<').replace('>', '>'))
- _OLD_TITLE_TITLE = '<title>Maritime QA Demo</title>'
- _OLD_TITLE_H1 = '<h1>Maritime QA Demo</h1>'
- _OLD_TAGLINE = '<p>RAG vs Wiki | 1326 docs | Qwen2.5:32B local</p>'
- _OLD_PLACEHOLDER = 'placeholder="Please enter a maritime question..."'
- _OLD_EXAMPLES = (
- ' <span onclick="askDirect(\'船舶进出港报告制度的主要内容是什么?\')">Ship entry/exit reporting system</span>\n'
- ' <span onclick="askDirect(\'船员适任证书的申请条件有哪些?\')">Crew competency certificates</span>\n'
- ' <span onclick="askDirect(\'琼州海峡船舶定线制是什么时候开始施行的?\')">Qiongzhou Strait routing system</span>\n'
- ' <span onclick="askDirect(\'海上交通安全法与水污染防治法在船舶管理方面的规定有何异同?\')">Maritime law comparison</span>\n'
- ' <span onclick="askDirect(\'中国船员发展报告从2019到2023年有什么变化趋势?\')">Crew development trends</span>'
- )
- def _apply_demo_config(html):
- html = html.replace(_OLD_TITLE_TITLE, '<title>' + _html_text_esc(DEMO_TITLE) + '</title>')
- html = html.replace(_OLD_TITLE_H1, '<h1>' + _html_text_esc(DEMO_TITLE) + '</h1>')
- html = html.replace(_OLD_TAGLINE, '<p>' + _html_text_esc(DEMO_TAGLINE) + '</p>')
- html = html.replace(_OLD_PLACEHOLDER, 'placeholder="' + _html_attr_esc(DEMO_PLACEHOLDER) + '"')
- if DEMO_EXAMPLES:
- lines = []
- for e in DEMO_EXAMPLES:
- q = str(e.get('q', ''))
- label = str(e.get('label', q))
- q_esc = _html_attr_esc(q)
- label_esc = _html_text_esc(label)
- lines.append(' <span onclick="askDirect(\'' + q_esc + '\')">' + label_esc + '</span>')
- html = html.replace(_OLD_EXAMPLES, '\n'.join(lines))
- # Inject default panel selections as JS
- init_script = (
- '\n<script>'
- 'document.getElementById("leftModeSelect").value = "' + _html_attr_esc(DEMO_LEFT_DEFAULT) + '";'
- 'document.getElementById("rightModeSelect").value = "' + _html_attr_esc(DEMO_RIGHT_DEFAULT) + '";'
- '</script>\n'
- )
- html = html.replace('</body>', init_script + '</body>')
- return html
- # LLM provider (lazy init)
- _provider = None
- def get_provider():
- global _provider
- if _provider is None:
- _provider = create_provider('ollama', model='qwen2.5-32b', base_url='http://127.0.0.1:8000/v1', timeout=120)
- return _provider
- # ── Conversation history persistence ──
- _history_lock = threading.Lock()
- def load_history():
- if os.path.exists(HISTORY_FILE):
- with open(HISTORY_FILE, 'r', encoding='utf-8') as f:
- return json.load(f)
- return []
- def save_record(record):
- with _history_lock:
- history = load_history()
- history.append(record)
- with open(HISTORY_FILE, 'w', encoding='utf-8') as f:
- json.dump(history, f, ensure_ascii=False, indent=2)
- # ── RAG answer ──
- def rag_answer(question, top_k=5):
- t0 = time.time()
- provider = get_provider()
- results = search(question, top_k=top_k)
- # Build enriched context from fusion results
- enriched = results[0].get('_enriched_context', '') if results else ''
- if not enriched:
- parts = []
- for i, r in enumerate(results):
- parts.append('[doc' + str(i+1) + ': ' + r['source'] + ']\n' + r['text'])
- enriched = '\n\n'.join(parts)
- enriched = enriched[:3500] # fit within 8192 context window
- _sys = _cfg.get('systemPrompt', '').strip()
- _sys_short = _sys[:500] if _sys else 'You are a ' + DEMO_DOMAIN + ' knowledge QA assistant.'
- prompt = f"""{_sys_short}
- 请严格基于以下参考资料回答问题。资料来自多个来源(检索文档、知识图谱、Wiki知识),请综合利用。
- 如果资料中没有相关信息,请说明"资料中未找到相关信息"。
- 回答要具体,引用原文,标注来源 [来源: 文件名]。
- {enriched}
- ## 问题
- {question}
- ## 回答"""
- answer = provider.chat([{'role': 'user', 'content': prompt}])
- elapsed = time.time() - t0
- # Extract unified search metadata
- query_type = results[0].get('_query_type', 'unknown') if results else 'unknown'
- strategy = results[0].get('_strategy', 'unknown') if results else 'unknown'
- search_time = results[0].get('_total_time', 0) if results else 0
- return {
- 'answer': answer,
- 'sources': [r['source'] for r in results],
- 'elapsed': round(elapsed, 1),
- 'chunks_used': len(results),
- 'mode': 'LambdaRAG',
- 'query_type': query_type,
- 'strategy': strategy,
- 'search_time': search_time,
- }
- # ── Wiki answer ──
- def wiki_answer(question, top_k=5):
- t0 = time.time()
- provider = get_provider()
- from pathlib import Path
- wiki_path = Path(WIKI_DIR)
- wiki_pages = []
- for subdir in ['sources', 'entities', 'topics', 'analyses']:
- d = wiki_path / subdir
- if d.exists():
- for f in d.glob('*.md'):
- try:
- content = f.read_text(encoding='utf-8')
- q_chars = set(question)
- match_score = sum(1 for c in q_chars if c in content)
- if match_score > len(question) * 0.3:
- wiki_pages.append({
- 'name': f.stem,
- 'content': content[:1500],
- 'score': match_score,
- })
- except:
- pass
- wiki_pages.sort(key=lambda x: x['score'], reverse=True)
- wiki_pages = wiki_pages[:3]
- if wiki_pages:
- wiki_context = '\n\n'.join([
- f'[Wiki: {p["name"]}]\n{p["content"]}' for p in wiki_pages
- ])
- source_type = 'wiki'
- else:
- results = search(question, top_k=top_k)
- compile_context = '\n\n'.join([
- f'[doc: {r["source"]}]\n{r["text"]}' for r in results
- ])
- compile_prompt = f"""请阅读以下文档片段,提炼出与问题相关的核心知识点。
- 用结构化的方式组织,标注来源。
- 文档:
- {compile_context}
- 问题: {question}
- 请输出结构化的知识摘要:"""
- compiled = provider.chat([{'role': 'user', 'content': compile_prompt}])
- wiki_context = compiled
- source_type = 'compiled'
- safe_name = question[:30].replace('/', '_').replace(' ', '_')
- analysis_path = wiki_path / 'analyses' / f'{safe_name}.md'
- analysis_path.parent.mkdir(parents=True, exist_ok=True)
- with open(analysis_path, 'w', encoding='utf-8') as f:
- f.write(f'# {question}\n\n{compiled}\n')
- answer_prompt = f"""You are a {DEMO_DOMAIN} wiki knowledge QA assistant.
- 基于以下已编译的 wiki 知识回答问题。回答要具体,标注来源。
- ## Wiki 知识
- {wiki_context}
- ## 问题
- {question}
- ## 回答"""
- answer = provider.chat([{'role': 'user', 'content': answer_prompt}])
- elapsed = time.time() - t0
- return {
- 'answer': answer,
- 'source_type': source_type,
- 'wiki_pages_used': len(wiki_pages),
- 'elapsed': round(elapsed, 1),
- 'mode': 'Wiki',
- }
- # ── Async results store ──
- # -- BM25 Baseline answer (for comparison) --
- def bm25_baseline_answer(question, top_k=5):
- t0 = time.time()
- provider = get_provider()
- try:
- from search_engine import search as bm25_only
- results = bm25_only(question, top_k=top_k)
- except Exception:
- results = []
- context_parts = []
- for i, r in enumerate(results):
- context_parts.append('[doc' + str(i+1) + ': ' + r['source'] + ']' + chr(10) + r['text'])
- context = (chr(10)*2).join(context_parts)
- prompt = ('You are a ' + DEMO_DOMAIN + ' QA assistant. Answer based on docs only.' + chr(10)
- + '## Docs' + chr(10) + context + chr(10)
- + '## Question' + chr(10) + question + chr(10) + '## Answer')
- answer = provider.chat([{'role': 'user', 'content': prompt}])
- elapsed = time.time() - t0
- return {
- 'answer': answer,
- 'sources': [r['source'] for r in results],
- 'elapsed': round(elapsed, 1),
- 'chunks_used': len(results),
- 'mode': 'BM25 Baseline',
- }
- # -- Direct LLM answer (no retrieval, pure parametric knowledge) --
- def direct_llm_answer(question):
- t0 = time.time()
- provider = get_provider()
- prompt = ('You are a ' + DEMO_DOMAIN + ' QA assistant. '
- 'Answer the following question using ONLY your own knowledge. '
- 'If you are not sure, say so.\n\n'
- '## Question\n' + question + '\n\n## Answer')
- answer = provider.chat([{'role': 'user', 'content': prompt}])
- elapsed = time.time() - t0
- return {
- 'answer': answer,
- 'elapsed': round(elapsed, 1),
- 'mode': 'Direct LLM',
- 'chunks_used': 0,
- 'sources': [],
- }
- # ── Lite answer (BM25 + Wiki combined — qaagent67lite strategy) ──
- def lite_answer(question, top_k=5):
- """qaagent67lite: BM25 关键词检索 + Wiki 编译知识融合"""
- t0 = time.time()
- provider = get_provider()
- from pathlib import Path
- # Path 1: BM25
- bm25_results = []
- try:
- from search_engine import search as bm25_search
- bm25_results = bm25_search(question, top_k=top_k)
- except Exception:
- pass
- bm25_context = '\n\n'.join([
- f'[BM25 doc: {r["source"]}]\n{r["text"]}' for r in bm25_results[:5]
- ]) if bm25_results else ''
- # Path 2: Wiki
- wiki_path = Path(WIKI_DIR)
- wiki_pages = []
- for subdir in ['entities', 'topics', 'sources', 'analyses']:
- d = wiki_path / subdir
- if d.exists():
- for f in d.glob('*.md'):
- try:
- content = f.read_text(encoding='utf-8')
- q_chars = set(question)
- match_score = sum(1 for c in q_chars if c in content)
- if match_score > len(question) * 0.3:
- wiki_pages.append({
- 'name': f.stem,
- 'subdir': subdir,
- 'content': content[:1500],
- 'score': match_score,
- })
- except Exception:
- pass
- wiki_pages.sort(key=lambda x: x['score'], reverse=True)
- wiki_pages = wiki_pages[:3]
- wiki_context = '\n\n'.join([
- f'[Wiki/{p["subdir"]}: {p["name"]}]\n{p["content"]}' for p in wiki_pages
- ]) if wiki_pages else ''
- # Merge contexts
- merged = ''
- if wiki_context:
- merged += '## Wiki 编译知识\n' + wiki_context + '\n\n'
- if bm25_context:
- merged += '## BM25 检索片段\n' + bm25_context
- if not merged.strip():
- return {
- 'answer': '知识库中未找到相关信息。',
- 'elapsed': round(time.time() - t0, 1),
- 'mode': 'Lite (BM25+Wiki)',
- 'bm25_chunks': 0,
- 'wiki_pages_used': 0,
- }
- _sys = _cfg.get('systemPrompt', '').strip()
- _sys_short = _sys[:500] if _sys else 'You are a ' + DEMO_DOMAIN + ' knowledge QA assistant.'
- prompt = f"""{_sys_short}
- 请基于以下两种来源回答问题。Wiki 编译知识是经过整理的结构化知识(优先参考),BM25 检索片段是原始文档片段(用于补充细节)。
- 如果两种来源有矛盾,以 Wiki 知识为准。标注信息来源。
- {merged}
- ## 问题
- {question}
- ## 回答"""
- answer = provider.chat([{'role': 'user', 'content': prompt}])
- elapsed = time.time() - t0
- return {
- 'answer': answer,
- 'elapsed': round(elapsed, 1),
- 'mode': 'Lite (BM25+Wiki)',
- 'bm25_chunks': len(bm25_results),
- 'wiki_pages_used': len(wiki_pages),
- 'sources': [r['source'] for r in bm25_results[:3]] + [p['name'] for p in wiki_pages],
- }
- _results = {}
- # ── API routes ──
- @app.route('/api/ask', methods=['POST'])
- def api_ask():
- data = request.json
- question = data.get('question', '').strip()
- if not question:
- return jsonify({'error': 'question is required'}), 400
- req_id = str(int(time.time() * 1000))
- _results[req_id] = {'lite': None, 'rag': None, 'wiki': None, 'question': question}
- def run_lite():
- try:
- _results[req_id]['lite'] = lite_answer(question)
- except Exception as e:
- _results[req_id]['lite'] = {'answer': f'Error: {e}', 'elapsed': 0, 'mode': 'Lite'}
- def run_rag():
- try:
- _results[req_id]['rag'] = rag_answer(question)
- except Exception as e:
- _results[req_id]['rag'] = {'answer': f'Error: {e}', 'elapsed': 0, 'mode': 'RAG'}
- def run_wiki():
- try:
- _results[req_id]['wiki'] = wiki_answer(question)
- except Exception as e:
- _results[req_id]['wiki'] = {'answer': f'Error: {e}', 'elapsed': 0, 'mode': 'Wiki'}
- def run_baseline():
- try:
- _results[req_id]['baseline'] = bm25_baseline_answer(question)
- except Exception as e:
- _results[req_id]['baseline'] = {'answer': 'Error: ' + str(e), 'elapsed': 0, 'mode': 'BM25 Baseline'}
- def run_direct():
- try:
- _results[req_id]['direct'] = direct_llm_answer(question)
- except Exception as e:
- _results[req_id]['direct'] = {'answer': 'Error: ' + str(e), 'elapsed': 0, 'mode': 'Direct LLM'}
- t0 = threading.Thread(target=run_lite)
- t1 = threading.Thread(target=run_rag)
- t2 = threading.Thread(target=run_wiki)
- t3 = threading.Thread(target=run_baseline)
- t4 = threading.Thread(target=run_direct)
- t0.start()
- t1.start()
- t2.start()
- t3.start()
- t4.start()
- return jsonify({'req_id': req_id, 'status': 'processing'})
- @app.route('/api/result/<req_id>')
- def api_result(req_id):
- if req_id not in _results:
- return jsonify({'error': 'not found'}), 404
- r = _results[req_id]
- # Done when all 5 modes have results
- done = all(r.get(k) is not None for k in ('lite', 'rag', 'wiki', 'baseline', 'direct'))
- # Save to history when all done
- if done and not r.get('_saved'):
- r['_saved'] = True
- record = {
- 'id': req_id,
- 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- 'question': r['question'],
- 'lite': r['lite'],
- 'rag': r['rag'],
- 'wiki': r.get('wiki'),
- 'baseline': r.get('baseline'),
- 'direct': r.get('direct'),
- }
- threading.Thread(target=save_record, args=(record,)).start()
- return jsonify({
- 'status': 'done' if done else 'processing',
- 'lite': r.get('lite'),
- 'rag': r.get('rag'),
- 'wiki': r.get('wiki'),
- 'baseline': r.get('baseline'),
- 'direct': r.get('direct'),
- })
- @app.route('/api/history')
- def api_history():
- history = load_history()
- history.reverse() # newest first
- return jsonify(history[:50]) # last 50
- @app.route('/api/history/export')
- def api_history_export():
- history = load_history()
- return jsonify({
- 'exported_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
- 'total_records': len(history),
- 'records': history,
- })
- @app.route('/api/wiki/index')
- def api_wiki_index():
- """List all wiki pages grouped by category"""
- from pathlib import Path
- wiki_path = Path(WIKI_DIR)
- result = {}
- for subdir in ['sources', 'entities', 'topics', 'analyses']:
- d = wiki_path / subdir
- pages = []
- if d.exists():
- for f in sorted(d.glob('*.md')):
- stat = f.stat()
- pages.append({
- 'name': f.stem,
- 'path': f'{subdir}/{f.name}',
- 'size': stat.st_size,
- 'modified': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M'),
- })
- result[subdir] = pages
- # Also read index.md
- idx_path = wiki_path / 'index.md'
- result['index_content'] = idx_path.read_text(encoding='utf-8') if idx_path.exists() else ''
- return jsonify(result)
- @app.route('/api/wiki/page/<path:page_path>')
- def api_wiki_page(page_path):
- """Read a single wiki page"""
- from pathlib import Path
- wiki_path = Path(WIKI_DIR)
- full_path = wiki_path / page_path
- if not full_path.exists() or not str(full_path).startswith(str(wiki_path)):
- return jsonify({'error': 'not found'}), 404
- content = full_path.read_text(encoding='utf-8')
- return jsonify({'path': page_path, 'content': content, 'size': len(content)})
- @app.route('/api/rag/search')
- def api_rag_search():
- """Search RAG chunks"""
- query = request.args.get('q', '').strip()
- top_k = int(request.args.get('k', 10))
- if not query:
- return jsonify({'error': 'q parameter required'}), 400
- results = search(query, top_k=top_k)
- return jsonify({'query': query, 'total_results': len(results), 'results': results})
- @app.route('/api/rag/stats')
- def api_rag_stats():
- """RAG index statistics"""
- import json as json_mod
- from pathlib import Path
- idx_path = Path(_knowledge.get('indexFile', KNOWLEDGE_BASE + '/rag_index.json'))
- if not idx_path.exists():
- return jsonify({'error': 'index not built'}), 404
- with open(idx_path) as f:
- data = json_mod.load(f)
- # Sample chunks for preview
- sample_chunks = data['chunks'][:20]
- for c in sample_chunks:
- pass # show full text
- return jsonify({
- 'total_chunks': data['total_chunks'],
- 'total_files': data['total_files'],
- 'chunk_size': data['chunk_size'],
- 'keyword_index_size': data.get('keyword_index_size', 0),
- 'sample_chunks': sample_chunks,
- })
- @app.route('/wiki')
- def wiki_page():
- return render_template_string(WIKI_HTML)
- @app.route('/rag')
- def rag_page():
- return render_template_string(RAG_HTML)
- @app.route('/')
- def index():
- return _apply_demo_config(render_template_string(HTML_TEMPLATE))
- HTML_TEMPLATE = r"""
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Maritime QA Demo</title>
- <style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; background: #f0f2f5; color: #333; }
- .header { background: linear-gradient(135deg, #1a3a5c 0%, #2d6aa0 100%); color: white; padding: 20px 30px; }
- .header-inner { max-width: 1400px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; }
- .header h1 { font-size: 22px; }
- .header p { opacity: 0.8; font-size: 13px; margin-top: 4px; }
- .header-stats { text-align: right; font-size: 13px; opacity: 0.8; }
- .container { max-width: 1400px; margin: 0 auto; padding: 20px; }
- .input-area { background: white; border-radius: 12px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
- .input-row { display: flex; gap: 12px; }
- .input-row input { flex: 1; padding: 12px 16px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 16px; outline: none; transition: border-color 0.2s; }
- .input-row input:focus { border-color: #2d6aa0; }
- .btn { padding: 12px 28px; background: #2d6aa0; color: white; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; transition: background 0.2s; white-space: nowrap; }
- .btn:hover { background: #1a5a8e; }
- .btn:disabled { background: #aaa; cursor: not-allowed; }
- .btn-sm { padding: 6px 14px; font-size: 13px; border-radius: 6px; }
- .btn-outline { background: transparent; border: 1px solid #2d6aa0; color: #2d6aa0; }
- .btn-outline:hover { background: #e0eef8; }
- .quick-questions { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 8px; }
- .quick-questions span { padding: 6px 12px; background: #f5f5f5; border-radius: 16px; font-size: 13px; cursor: pointer; transition: background 0.2s; border: 1px solid transparent; }
- .quick-questions span:hover { background: #e0eef8; border-color: #c0d8f0; }
- .compare-area { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
- .panel { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); min-height: 200px; }
- .panel-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 2px solid #f0f0f0; }
- .panel-title { font-size: 18px; font-weight: 600; }
- .panel-title.rag { color: #2d8a5e; }
- .panel-title.wiki { color: #7b2d8e; }
- .badge-lite { background: #e0f0e8; color: #2d8a5e; }
- .panel-badge { padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: 500; }
- .badge-rag { background: #e0eef8; color: #2d6aa0; }
- .badge-wiki { background: #f0e0f8; color: #7b2d8e; }
- .panel-content { font-size: 15px; line-height: 1.8; white-space: pre-wrap; word-wrap: break-word; max-height: 600px; overflow-y: auto; }
- .panel-meta { margin-top: 12px; padding-top: 12px; border-top: 1px solid #f0f0f0; font-size: 13px; color: #888; }
- .loading { text-align: center; padding: 40px; color: #888; }
- .spinner { display: inline-block; width: 20px; height: 20px; border: 3px solid #e0e0e0; border-top-color: #2d6aa0; border-radius: 50%; animation: spin 0.8s linear infinite; margin-right: 8px; vertical-align: middle; }
- @keyframes spin { to { transform: rotate(360deg); } }
- .tabs { display: flex; gap: 0; margin-top: 20px; margin-bottom: 0; }
- .tab { padding: 10px 20px; background: #e8e8e8; cursor: pointer; font-size: 14px; border-radius: 8px 8px 0 0; }
- .tab.active { background: white; font-weight: 600; }
- .tab-content { background: white; border-radius: 0 12px 12px 12px; padding: 16px 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); display: none; max-height: 400px; overflow-y: auto; }
- .tab-content.active { display: block; }
- .hist-item { padding: 12px 0; border-bottom: 1px solid #f5f5f5; cursor: pointer; }
- .hist-item:hover { background: #fafafa; }
- .hist-item:last-child { border-bottom: none; }
- .hist-q { font-weight: 600; font-size: 14px; margin-bottom: 4px; }
- .hist-meta { font-size: 12px; color: #999; }
- .empty-hint { text-align: center; color: #ccc; padding: 30px; }
- .mode-select { padding: 4px 10px; border-radius: 8px; border: 1px solid #7b2d8e; background: #f0e0f8; color: #7b2d8e; font-size: 13px; font-weight: 500; cursor: pointer; outline: none; }
- .mode-select:focus { border-color: #5a1a6e; }
- .mode-select-left { border-color: #2d6aa0; background: #e0eef8; color: #2d6aa0; }
- .mode-select-left:focus { border-color: #1a5a8e; }
- .badge-left { background: #e0eef8; color: #2d6aa0; }
- @media (max-width: 768px) { .compare-area { grid-template-columns: 1fr; } }
- </style>
- </head>
- <body>
- <div class="header">
- <div class="header-inner">
- <div>
- <h1>Maritime QA Demo</h1>
- <p>RAG vs Wiki | 1326 docs | Qwen2.5:32B local</p>
- </div>
- <div class="header-stats">
- <div style="margin-bottom:6px;">
- <a href="/" style="color:white;text-decoration:none;margin-right:16px;font-weight:600;">QA Demo</a>
- <a href="/wiki" style="color:white;text-decoration:none;margin-right:16px;opacity:0.85;">Wiki Browser</a>
- <a href="/rag" style="color:white;text-decoration:none;opacity:0.85;">RAG Chunks</a>
- </div>
- <div id="statsLine">-</div>
- </div>
- </div>
- </div>
- <div class="container">
- <div class="input-area">
- <div class="input-row">
- <input type="text" id="question" placeholder="Please enter a maritime question..." autofocus>
- <button class="btn" id="askBtn" onclick="askQuestion()">Ask</button>
- </div>
- <div class="quick-questions">
- <span onclick="askDirect('船舶进出港报告制度的主要内容是什么?')">Ship entry/exit reporting system</span>
- <span onclick="askDirect('船员适任证书的申请条件有哪些?')">Crew competency certificates</span>
- <span onclick="askDirect('琼州海峡船舶定线制是什么时候开始施行的?')">Qiongzhou Strait routing system</span>
- <span onclick="askDirect('海上交通安全法与水污染防治法在船舶管理方面的规定有何异同?')">Maritime law comparison</span>
- <span onclick="askDirect('中国船员发展报告从2019到2023年有什么变化趋势?')">Crew development trends</span>
- </div>
- </div>
- <div class="compare-area">
- <div class="panel" id="leftPanel">
- <div class="panel-header">
- <select id="leftModeSelect" class="mode-select mode-select-left" onchange="onPanelModeChange('left')">
- <option value="lite">Lite (BM25 + Wiki)</option>
- <option value="rag">LambdaRAG (4-path)</option>
- <option value="wiki">Wiki Only</option>
- <option value="baseline">BM25 Baseline</option>
- <option value="direct">Direct LLM</option>
- </select>
- <span class="panel-badge badge-left" id="leftBadge">-</span>
- </div>
- <div class="panel-content" id="leftContent" style="white-space:pre-wrap;">
- <div class="empty-hint">Ask to see response</div>
- </div>
- <div class="panel-meta" id="leftMeta"></div>
- </div>
- <div class="panel" id="rightPanel">
- <div class="panel-header">
- <select id="rightModeSelect" class="mode-select" onchange="onPanelModeChange('right')">
- <option value="rag">LambdaRAG (4-path)</option>
- <option value="lite">Lite (BM25 + Wiki)</option>
- <option value="wiki">Wiki Only</option>
- <option value="baseline">BM25 Baseline</option>
- <option value="direct">Direct LLM</option>
- </select>
- <span class="panel-badge badge-wiki" id="rightBadge">-</span>
- </div>
- <div class="panel-content" id="rightContent" style="white-space:pre-wrap;">
- <div class="empty-hint">Ask to see comparison</div>
- </div>
- <div class="panel-meta" id="rightMeta"></div>
- </div>
- </div>
- <div class="tabs">
- <div class="tab active" onclick="switchTab('history')">History</div>
- <div class="tab" onclick="switchTab('export')">Export</div>
- </div>
- <div class="tab-content active" id="tab-history">
- <div class="empty-hint" id="historyEmpty">No history yet</div>
- <div id="historyList"></div>
- </div>
- <div class="tab-content" id="tab-export">
- <p style="margin-bottom:12px;color:#666;">All records are saved on the server. Click to export.</p>
- <button class="btn btn-sm btn-outline" onclick="exportHistory()">Export JSON</button>
- <pre id="exportPreview" style="margin-top:12px;font-size:12px;max-height:300px;overflow:auto;background:#f8f8f8;padding:12px;border-radius:8px;display:none;"></pre>
- </div>
- </div>
- <script>
- const qInput = document.getElementById('question');
- const askBtn = document.getElementById('askBtn');
- qInput.addEventListener('keydown', e => { if (e.key === 'Enter') askQuestion(); });
- function askDirect(q) { qInput.value = q; askQuestion(); }
- function askQuestion() {
- const q = qInput.value.trim();
- if (!q) return;
- askBtn.disabled = true;
- askBtn.textContent = 'Processing...';
- document.getElementById('leftContent').innerHTML = '<div class="loading"><span class="spinner"></span>Loading...</div>';
- document.getElementById('rightContent').innerHTML = '<div class="loading"><span class="spinner"></span>Loading...</div>';
- document.getElementById('leftMeta').textContent = '';
- document.getElementById('rightMeta').textContent = '';
- fetch('/api/ask', {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({question: q})
- }).then(r => r.json()).then(data => pollResult(data.req_id, q));
- }
- var _currentResult = null;
- function displayPanel(side, data) {
- var selectId = side + 'ModeSelect';
- var contentId = side + 'Content';
- var badgeId = side + 'Badge';
- var metaId = side + 'Meta';
- var mode = document.getElementById(selectId).value;
- var src = data[mode] || null;
- if (!src) {
- document.getElementById(contentId).innerHTML = '<div class="loading"><span class="spinner"></span>Loading ' + mode + '...</div>';
- document.getElementById(metaId).textContent = '';
- return;
- }
- document.getElementById(contentId).textContent = src.answer;
- var srcs = src.sources ? src.sources.slice(0,3).join(', ') : '';
- if (mode === 'rag') {
- document.getElementById(badgeId).textContent = (src.query_type||'') + ' → ' + (src.strategy||'Unified');
- document.getElementById(metaId).innerHTML =
- '<b>Time:</b> ' + src.elapsed + 's | <b>Strategy:</b> ' + (src.strategy||'-') +
- ' | <b>Chunks:</b> ' + (src.chunks_used||0) + '<br><b>Sources:</b> ' + srcs;
- } else if (mode === 'lite') {
- document.getElementById(badgeId).textContent =
- 'BM25:' + (src.bm25_chunks||0) + ' + Wiki:' + (src.wiki_pages_used||0);
- document.getElementById(metaId).innerHTML =
- '<b>Time:</b> ' + src.elapsed + 's | <b>BM25:</b> ' + (src.bm25_chunks||0) +
- ' | <b>Wiki:</b> ' + (src.wiki_pages_used||0) + '<br><b>Sources:</b> ' + srcs;
- } else if (mode === 'wiki') {
- document.getElementById(badgeId).textContent = 'Compile + Query';
- document.getElementById(metaId).innerHTML =
- '<b>Time:</b> ' + src.elapsed + 's | <b>Wiki pages:</b> ' + (src.wiki_pages_used||0);
- } else if (mode === 'baseline') {
- document.getElementById(badgeId).textContent = 'BM25 Keyword';
- document.getElementById(metaId).innerHTML =
- '<b>Time:</b> ' + src.elapsed + 's | <b>Chunks:</b> ' + (src.chunks_used||0) +
- ' | <b>Sources:</b> ' + srcs;
- } else if (mode === 'direct') {
- document.getElementById(badgeId).textContent = 'No Retrieval';
- document.getElementById(metaId).innerHTML =
- '<b>Time:</b> ' + src.elapsed + 's | <b>Mode:</b> Pure LLM';
- }
- }
- function onPanelModeChange(side) {
- if (_currentResult) displayPanel(side, _currentResult);
- }
- function pollResult(reqId, question) {
- const poll = setInterval(() => {
- fetch('/api/result/' + reqId).then(r => r.json()).then(data => {
- _currentResult = data;
- displayPanel('left', data);
- displayPanel('right', data);
- if (data.status === 'done') {
- clearInterval(poll);
- askBtn.disabled = false;
- askBtn.textContent = 'Ask';
- loadHistory();
- }
- });
- }, 2000);
- }
- function loadHistory() {
- fetch('/api/history').then(r => r.json()).then(data => {
- const el = document.getElementById('historyList');
- const empty = document.getElementById('historyEmpty');
- if (!data.length) { empty.style.display = 'block'; el.innerHTML = ''; return; }
- empty.style.display = 'none';
- document.getElementById('statsLine').textContent = 'Total records: ' + data.length;
- el.innerHTML = data.map((h, i) =>
- '<div class="hist-item" onclick="showRecord(' + i + ')">' +
- '<div class="hist-q">' + escHtml(h.question) + '</div>' +
- '<div class="hist-meta">' + h.timestamp +
- ' | Lite: ' + (h.lite?h.lite.elapsed:'-') + 's' +
- ' | RAG: ' + (h.rag?h.rag.elapsed:'-') + 's</div></div>'
- ).join('');
- window._histData = data;
- });
- }
- function showRecord(idx) {
- const h = window._histData[idx];
- if (!h) return;
- qInput.value = h.question;
- _currentResult = h;
- displayPanel('left', h);
- displayPanel('right', h);
- window.scrollTo({top: 0, behavior: 'smooth'});
- }
- function switchTab(name) {
- document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
- document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
- event.target.classList.add('active');
- document.getElementById('tab-' + name).classList.add('active');
- }
- function exportHistory() {
- fetch('/api/history/export').then(r => r.json()).then(data => {
- const pre = document.getElementById('exportPreview');
- pre.style.display = 'block';
- pre.textContent = JSON.stringify(data, null, 2);
- });
- }
- function escHtml(s) {
- const d = document.createElement('div');
- d.textContent = s;
- return d.innerHTML;
- }
- function renderAnswer(text) {
- if (typeof marked === "undefined") return escHtml(text);
- var clean = text.replace(/([^\n])\n([^\n\-\#\*\|])/g, "$1\n\n$2");
- clean = clean.replace(/([^\n])\n(- )/g, "$1\n\n$2");
- clean = clean.replace(/([^\n])\n(#{1,3} )/g, "$1\n\n$2");
- try { return marked.parse(clean); } catch(e) { return "<pre>" + escHtml(text) + "</pre>"; }
- }
- // Load history on page load
- loadHistory();
- </script>
- </body>
- </html>
- """
- WIKI_HTML = r"""
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Wiki Browser</title>
- <style>
- * { margin:0; padding:0; box-sizing:border-box; }
- body { font-family: -apple-system,"PingFang SC","Microsoft YaHei",sans-serif; background:#f0f2f5; color:#333; }
- .header { background:linear-gradient(135deg,#5b2d8e 0%,#7b4dae 100%); color:white; padding:16px 30px; }
- .header-inner { max-width:1400px; margin:0 auto; display:flex; justify-content:space-between; align-items:center; }
- .header h1 { font-size:22px; }
- .header a { color:white; text-decoration:none; opacity:0.8; }
- .header a:hover { opacity:1; }
- .container { max-width:1400px; margin:0 auto; padding:20px; display:flex; gap:20px; }
- .sidebar { width:300px; flex-shrink:0; }
- .main { flex:1; min-width:0; }
- .card { background:white; border-radius:12px; padding:16px; margin-bottom:16px; box-shadow:0 2px 8px rgba(0,0,0,0.08); }
- .card h3 { font-size:15px; margin-bottom:10px; color:#5b2d8e; }
- .page-list { max-height:250px; overflow-y:auto; }
- .page-item { padding:6px 8px; cursor:pointer; font-size:13px; border-radius:6px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
- .page-item:hover { background:#f0e0f8; }
- .page-item.active { background:#e0d0f0; font-weight:600; }
- .badge { display:inline-block; padding:2px 8px; border-radius:10px; font-size:11px; background:#f0e0f8; color:#5b2d8e; margin-left:6px; }
- .content-area { font-size:15px; line-height:1.8; word-wrap:break-word; max-height:700px; overflow-y:auto; }
- .content-area h1 { font-size:20px; margin:16px 0 8px; color:#333; border-bottom:1px solid #eee; padding-bottom:6px; }
- .content-area h2 { font-size:17px; margin:14px 0 6px; color:#444; }
- .content-area h3 { font-size:15px; margin:10px 0 4px; color:#555; }
- .content-area ul, .content-area ol { padding-left:20px; margin:8px 0; }
- .content-area li { margin:4px 0; }
- .content-area p { margin:8px 0; }
- .content-area code { background:#f0f0f0; padding:1px 4px; border-radius:3px; font-size:13px; }
- .content-area blockquote { border-left:3px solid #ccc; padding-left:12px; color:#666; margin:8px 0; }
- .stats-row { display:flex; gap:12px; margin-bottom:16px; }
- .stat-box { flex:1; background:white; border-radius:10px; padding:14px; text-align:center; box-shadow:0 1px 4px rgba(0,0,0,0.06); }
- .stat-num { font-size:28px; font-weight:700; color:#5b2d8e; }
- .stat-label { font-size:12px; color:#888; margin-top:4px; }
- .search-box { width:100%; padding:8px 12px; border:1px solid #ddd; border-radius:6px; font-size:14px; margin-bottom:10px; }
- </style>
- </head>
- <body>
- <div class="header">
- <div class="header-inner">
- <h1>Wiki Browser</h1>
- <div><a href="/">QA Demo</a> | <a href="/rag">RAG Chunks</a></div>
- </div>
- </div>
- <div class="container">
- <div class="sidebar">
- <div class="stats-row">
- <div class="stat-box"><div class="stat-num" id="nSources">-</div><div class="stat-label">Sources</div></div>
- <div class="stat-box"><div class="stat-num" id="nEntities">-</div><div class="stat-label">Entities</div></div>
- <div class="stat-box"><div class="stat-num" id="nTopics">-</div><div class="stat-label">Topics</div></div>
- </div>
- <div class="card"><h3>Sources</h3><input class="search-box" placeholder="Filter..." oninput="filterList('sources',this.value)"><div class="page-list" id="list-sources"></div></div>
- <div class="card"><h3>Entities</h3><input class="search-box" placeholder="Filter..." oninput="filterList('entities',this.value)"><div class="page-list" id="list-entities"></div></div>
- <div class="card"><h3>Topics</h3><div class="page-list" id="list-topics"></div></div>
- <div class="card"><h3>Analyses</h3><div class="page-list" id="list-analyses"></div></div>
- </div>
- <div class="main">
- <div class="card">
- <h3 id="pageTitle">Wiki Index</h3>
- <div class="content-area" id="pageContent">Loading...</div>
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
- </div>
- </div>
- </div>
- <script>
- let wikiData = {};
- function loadWiki() {
- fetch('/api/wiki/index').then(r=>r.json()).then(data => {
- wikiData = data;
- document.getElementById('nSources').textContent = data.sources.length;
- document.getElementById('nEntities').textContent = data.entities.length;
- document.getElementById('nTopics').textContent = data.topics.length;
- renderList('sources', data.sources);
- renderList('entities', data.entities);
- renderList('topics', data.topics);
- renderList('analyses', data.analyses);
- const idxContent = data.index_content || 'Wiki is empty. Run wiki_compile.py to populate.';
- if (typeof marked !== 'undefined') {
- document.getElementById('pageContent').innerHTML = marked.parse(idxContent);
- } else {
- document.getElementById('pageContent').textContent = idxContent;
- }
- });
- }
- function renderList(cat, pages) {
- const el = document.getElementById('list-' + cat);
- el.innerHTML = pages.map(p =>
- '<div class="page-item" data-path="' + p.path + '" data-name="' + p.name + '" onclick="loadPage(\'' + p.path + '\',\'' + escAttr(p.name) + '\')">' +
- p.name.substring(0, 50) + '<span class="badge">' + (p.size/1024).toFixed(1) + 'K</span></div>'
- ).join('');
- }
- function loadPage(path, name) {
- document.querySelectorAll('.page-item').forEach(e => e.classList.remove('active'));
- document.querySelectorAll('[data-path="'+path+'"]').forEach(e => e.classList.add('active'));
- document.getElementById('pageTitle').textContent = name;
- document.getElementById('pageContent').innerHTML = '<div style="color:#aaa">Loading...</div>';
- fetch('/api/wiki/page/' + path).then(r=>r.json()).then(data => {
- if (typeof marked !== 'undefined') {
- document.getElementById('pageContent').innerHTML = marked.parse(data.content);
- } else {
- document.getElementById('pageContent').textContent = data.content;
- }
- });
- }
- function filterList(cat, q) {
- const items = document.querySelectorAll('#list-' + cat + ' .page-item');
- q = q.toLowerCase();
- items.forEach(el => { el.style.display = el.dataset.name.toLowerCase().includes(q) ? '' : 'none'; });
- }
- function escAttr(s) { return s.replace(/'/g, "\\'"); }
- loadWiki();
- </script>
- </body>
- </html>
- """
- RAG_HTML = r"""
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>RAG Chunks Browser</title>
- <style>
- * { margin:0; padding:0; box-sizing:border-box; }
- body { font-family: -apple-system,"PingFang SC","Microsoft YaHei",sans-serif; background:#f0f2f5; color:#333; }
- .header { background:linear-gradient(135deg,#1a3a5c 0%,#2d6aa0 100%); color:white; padding:16px 30px; }
- .header-inner { max-width:1400px; margin:0 auto; display:flex; justify-content:space-between; align-items:center; }
- .header h1 { font-size:22px; }
- .header a { color:white; text-decoration:none; opacity:0.8; }
- .header a:hover { opacity:1; }
- .container { max-width:1400px; margin:0 auto; padding:20px; }
- .stats-row { display:flex; gap:12px; margin-bottom:20px; }
- .stat-box { flex:1; background:white; border-radius:10px; padding:16px; text-align:center; box-shadow:0 1px 4px rgba(0,0,0,0.06); }
- .stat-num { font-size:32px; font-weight:700; color:#2d6aa0; }
- .stat-label { font-size:12px; color:#888; margin-top:4px; }
- .search-area { background:white; border-radius:12px; padding:20px; margin-bottom:20px; box-shadow:0 2px 8px rgba(0,0,0,0.08); }
- .search-row { display:flex; gap:12px; }
- .search-row input { flex:1; padding:12px 16px; border:2px solid #e0e0e0; border-radius:8px; font-size:16px; outline:none; }
- .search-row input:focus { border-color:#2d6aa0; }
- .btn { padding:12px 24px; background:#2d6aa0; color:white; border:none; border-radius:8px; font-size:15px; cursor:pointer; }
- .btn:hover { background:#1a5a8e; }
- .chunk-card { background:white; border-radius:10px; padding:16px; margin-bottom:12px; box-shadow:0 1px 4px rgba(0,0,0,0.06); border-left:4px solid #2d6aa0; }
- .chunk-header { display:flex; justify-content:space-between; margin-bottom:8px; font-size:13px; color:#888; }
- .chunk-source { font-weight:600; color:#2d6aa0; }
- .chunk-score { background:#e0eef8; padding:2px 8px; border-radius:10px; font-size:12px; }
- .chunk-text { font-size:14px; line-height:1.7; white-space:pre-wrap; word-wrap:break-word; max-height:200px; overflow-y:auto; background:#fafafa; padding:10px; border-radius:6px; }
- .sample-title { font-size:16px; font-weight:600; margin:20px 0 12px; color:#555; }
- </style>
- </head>
- <body>
- <div class="header">
- <div class="header-inner">
- <h1>RAG Chunks Browser</h1>
- <div><a href="/">QA Demo</a> | <a href="/wiki">Wiki</a></div>
- </div>
- </div>
- <div class="container">
- <div class="stats-row">
- <div class="stat-box"><div class="stat-num" id="nChunks">-</div><div class="stat-label">Total Chunks</div></div>
- <div class="stat-box"><div class="stat-num" id="nFiles">-</div><div class="stat-label">Source Files</div></div>
- <div class="stat-box"><div class="stat-num" id="nChunkSize">-</div><div class="stat-label">Chunk Size</div></div>
- <div class="stat-box"><div class="stat-num" id="nKeywords">-</div><div class="stat-label">Keywords</div></div>
- </div>
- <div class="search-area">
- <div class="search-row">
- <input type="text" id="searchInput" placeholder="Search RAG chunks (e.g. ship entry/exit reporting)...">
- <select id="topK" style="padding:10px;border-radius:8px;border:1px solid #ddd;font-size:14px;">
- <option value="5">Top 5</option>
- <option value="10" selected>Top 10</option>
- <option value="20">Top 20</option>
- <option value="50">Top 50</option>
- </select>
- <button class="btn" onclick="doSearch()">Search</button>
- </div>
- </div>
- <div id="results"></div>
- <div class="sample-title" id="sampleTitle">Sample Chunks (from index)</div>
- <div id="sampleChunks"></div>
- </div>
- <script>
- document.getElementById('searchInput').addEventListener('keydown', e => { if(e.key==='Enter') doSearch(); });
- function loadStats() {
- fetch('/api/rag/stats').then(r=>r.json()).then(data => {
- document.getElementById('nChunks').textContent = data.total_chunks.toLocaleString();
- document.getElementById('nFiles').textContent = data.total_files.toLocaleString();
- document.getElementById('nChunkSize').textContent = data.chunk_size;
- document.getElementById('nKeywords').textContent = data.keyword_index_size.toLocaleString();
- const el = document.getElementById('sampleChunks');
- el.innerHTML = data.sample_chunks.map(c => renderChunk(c, null)).join('');
- });
- }
- function doSearch() {
- const q = document.getElementById('searchInput').value.trim();
- if (!q) return;
- const k = document.getElementById('topK').value;
- document.getElementById('results').innerHTML = '<div style="text-align:center;padding:20px;color:#888;">Searching...</div>';
- document.getElementById('sampleTitle').style.display = 'none';
- document.getElementById('sampleChunks').style.display = 'none';
- fetch('/api/rag/search?q=' + encodeURIComponent(q) + '&k=' + k)
- .then(r=>r.json()).then(data => {
- const el = document.getElementById('results');
- if (!data.results.length) {
- el.innerHTML = '<div style="text-align:center;padding:20px;color:#888;">No results found</div>';
- return;
- }
- el.innerHTML = '<div style="margin-bottom:12px;font-size:14px;color:#666;">Found ' + data.results.length + ' chunks for "<b>' + escHtml(q) + '</b>"</div>' +
- data.results.map(c => renderChunk(c, c.score)).join('');
- });
- }
- function renderChunk(c, score) {
- return '<div class="chunk-card">' +
- '<div class="chunk-header"><span class="chunk-source">' + escHtml(c.source) +
- ' #' + c.chunk_id + '</span>' +
- (score !== null ? '<span class="chunk-score">Score: ' + score + '</span>' : '') +
- '</div><div class="chunk-text">' + escHtml(c.text) + '</div></div>';
- }
- function escHtml(s) { const d=document.createElement('div'); d.textContent=s; return d.innerHTML; }
- loadStats();
- </script>
- </body>
- </html>
- """
- if __name__ == '__main__':
- print("Loading search index...")
- search("test", top_k=1) # warm up
- print("Index loaded. Starting server...")
- print("Access: http://" + DEMO_BIND + ":" + str(DEMO_PORT))
- app.run(host=DEMO_BIND, port=DEMO_PORT, debug=False, threaded=True)
|