| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884 |
- #!/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)
- _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 = _wiki_cfg.get('wiki', {}).get('dir', '/app/agentexample/qaagent67wiki/wiki')
- # 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
- prompt = f"""你是一个海事领域问答助手。请严格基于以下参考资料回答问题。
- 资料来自多个来源(检索文档、知识图谱、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"""你是一个海事领域 wiki 知识库问答助手。
- 基于以下已编译的 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 maritime 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',
- }
- _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] = {'rag': None, 'wiki': None, 'question': question}
- 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'}
- t1 = threading.Thread(target=run_rag)
- t2 = threading.Thread(target=run_wiki)
- t3 = threading.Thread(target=run_baseline)
- t1.start()
- t2.start()
- t3.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 = r['rag'] is not None and r['wiki'] is not None
- # Save to history when both are 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'],
- 'rag': r['rag'],
- 'wiki': r['wiki'],
- }
- threading.Thread(target=save_record, args=(record,)).start()
- return jsonify({
- 'status': 'done' if done else 'processing',
- 'rag': r['rag'],
- 'wiki': r['wiki'],
- 'baseline': r.get('baseline'),
- })
- @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 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: #2d6aa0; }
- .panel-title.wiki { color: #7b2d8e; }
- .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; }
- @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="ragPanel">
- <div class="panel-header">
- <span class="panel-title rag">LambdaRAG (Unified)</span>
- <span class="panel-badge badge-rag" id="ragBadge">Classify + Route + Retrieve</span>
- </div>
- <div class="panel-content" id="ragContent" style="white-space:pre-wrap;">
- <div class="empty-hint">Ask to see RAG response</div>
- </div>
- <div class="panel-meta" id="ragMeta"></div>
- </div>
- <div class="panel" id="wikiPanel">
- <div class="panel-header">
- <span class="panel-title wiki">Wiki Mode</span>
- <span class="panel-badge badge-wiki">Compile + Query</span>
- </div>
- <div class="panel-content" id="wikiContent" style="white-space:pre-wrap;">
- <div class="empty-hint">Ask to see Wiki response</div>
- </div>
- <div class="panel-meta" id="wikiMeta"></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('ragContent').innerHTML = '<div class="loading"><span class="spinner"></span>RAG retrieving...</div>';
- document.getElementById('wikiContent').innerHTML = '<div class="loading"><span class="spinner"></span>Wiki compiling...</div>';
- document.getElementById('ragMeta').textContent = '';
- document.getElementById('wikiMeta').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));
- }
- function pollResult(reqId, question) {
- const poll = setInterval(() => {
- fetch('/api/result/' + reqId).then(r => r.json()).then(data => {
- if (data.rag) {
- document.getElementById("ragContent").textContent = data.rag.answer;
- const srcs = data.rag.sources ? data.rag.sources.slice(0,3).join(', ') : '';
- var qtype = data.rag.query_type || 'unknown';
- var strat = data.rag.strategy || 'unknown';
- var stime = data.rag.search_time || 0;
- document.getElementById('ragBadge').textContent = qtype + ' → ' + strat;
- document.getElementById('ragMeta').innerHTML =
- '<b>Type:</b> ' + qtype + ' | <b>Strategy:</b> ' + strat +
- '<br><b>Time:</b> ' + data.rag.elapsed + 's (search: ' + stime + 's) | <b>Chunks:</b> ' + (data.rag.chunks_used||0) +
- '<br><b>Sources:</b> ' + srcs;
- }
- if (data.wiki) {
- document.getElementById("wikiContent").textContent = data.wiki.answer;
- document.getElementById('wikiMeta').innerHTML =
- '<b>Time:</b> ' + data.wiki.elapsed + 's | <b>Source type:</b> ' + (data.wiki.source_type||'wiki') +
- ' | <b>Wiki pages:</b> ' + (data.wiki.wiki_pages_used||0);
- }
- 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 +
- ' | RAG: ' + (h.rag?h.rag.elapsed:'-') + 's' +
- ' | Wiki: ' + (h.wiki?h.wiki.elapsed:'-') + 's</div></div>'
- ).join('');
- window._histData = data;
- });
- }
- function showRecord(idx) {
- const h = window._histData[idx];
- if (!h) return;
- qInput.value = h.question;
- if (h.rag) {
- document.getElementById("ragContent").textContent = h.rag.answer;
- const srcs = h.rag.sources ? h.rag.sources.slice(0,3).join(', ') : '';
- document.getElementById('ragMeta').innerHTML = '<b>Time:</b> ' + h.rag.elapsed + 's | <b>Sources:</b> ' + srcs;
- }
- if (h.wiki) {
- document.getElementById("wikiContent").textContent = h.wiki.answer;
- document.getElementById('wikiMeta').innerHTML = '<b>Time:</b> ' + h.wiki.elapsed + 's | <b>Source:</b> ' + (h.wiki.source_type||'wiki');
- }
- 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://0.0.0.0:8080")
- app.run(host='0.0.0.0', port=8080, debug=False, threaded=True)
|