rag/wwwroot/knowledge_bases_list/search_result.dspy

99 lines
4.5 KiB
Plaintext

ns = params_kw.copy()
kb_id = ns.get('kb_id', '')
env = request._run_ns
query = ns.get("query") or ns.get("value") or ns.get("keyword") or ns.get("text") or ""
file_data = None
file_name = None
try:
reader = await request.multipart()
while True:
part = await reader.next()
if part is None: break
if part.name == "file": file_data = await part.read(); file_name = part.filename; break
except: pass
if not file_data:
try: file_data = await request.read();
except: pass
if file_data and len(file_data) < 10: file_data = None
import base64, io
top_k = int(ns.get('top_k', 5))
if not query:
return json.dumps({
"widgettype": "VBox", "options": {"padding": "20px", "spacing": "16px"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "🔍 知识检索", "cfontsize": 20, "fontWeight": "bold"}},
{"widgettype": "Text", "options": {"text": "请输入检索内容", "color": "#666", "marginTop": "20px"}}
]
}, ensure_ascii=False)
import aiohttp
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=20)) as s:
# 1. Embed
r = await s.post('https://embedding.opencomputing.net:10443/api/embed',
json={"texts": [query], "model": "CLIP-ViT-H-14"})
emb = await r.json() if r.status == 200 else {}
vec = emb.get("text_embeddings", emb.get("embeddings", [[]]))[0]
if not vec:
return json.dumps({"widgettype": "Text", "options": {"text": "向量化失败", "cfontsize": 14, "color": "#e74c3c"}}, ensure_ascii=False)
# 2. VDB search
recall_n = top_k * 3
r2 = await s.post('https://vectordb.opencomputing.net:10443/v1/query',
json={"colname": kb_id, "vector": vec, "pagerows": recall_n, "output_fields": ["*"]})
vdb = await r2.json() if r2.status == 200 else {}
raw_rows = vdb.get("data", {}).get("rows", [])
if not isinstance(raw_rows, list):
raw_rows = []
hits = []
for row in raw_rows:
rid = str(row.get("id", ""))
score = row.get("score", 0)
# Resolve doc_id from chunk id pattern "xxxx_c0"
doc_id = rid.rsplit('_c', 1)[0] if '_c' in rid else rid
# Look up chunk text from DB
chunk_text = ''
doc_name = ''
async with get_sor_context(env, 'rag') as sor:
recs = await sor.sqlExe(
"SELECT content, doc_id FROM document_chunks WHERE id=${id}$",
{"id": rid})
if recs:
chunk_text = recs[0].content or ''
doc_id = recs[0].doc_id or doc_id
if chunk_text:
hits.append({"id": rid, "doc_id": doc_id, "score": score, "text": chunk_text})
hits.sort(key=lambda x: x.get("score", 0), reverse=True)
hits = hits[:top_k]
subwidgets = [
{"widgettype": "Text", "options": {"text": f"🔍 检索: {query}", "cfontsize": 18, "fontWeight": "bold", "marginBottom": "8px"}},
{"widgettype": "Text", "options": {"text": f"共 {len(hits)} 条结果 (召回 {len(raw_rows)} 条)", "cfontsize": 13, "color": "#888", "marginBottom": "16px"}}
]
if not hits:
subwidgets.append({"widgettype": "Text", "options": {"text": "😔 未找到相关内容", "cfontsize": 14, "color": "#aaa", "halign": "center", "marginTop": "40px"}})
else:
for i, h in enumerate(hits):
score_pct = round(float(h["score"]) * 100, 1)
color = "#3b82f6" if score_pct > 60 else ("#10b981" if score_pct > 30 else "#f59e0b")
subwidgets.append({
"widgettype": "VBox",
"options": {"padding": "12px 16px", "marginBottom": "8px", "border": "1px solid #e0e0e0", "borderLeft": f"3px solid {color}", "bgcolor": "#fafafa", "borderRadius": "4px"},
"subwidgets": [
{"widgettype": "HBox", "options": {"alignItems": "center", "marginBottom": "6px"}, "subwidgets": [
{"widgettype": "Text", "options": {"text": f"#{i+1}", "cfontsize": 12, "fontWeight": "bold", "color": color, "marginRight": "8px"}},
{"widgettype": "Text", "options": {"text": f"{score_pct}%", "cfontsize": 11, "bgcolor": color, "color": "#fff", "padding": "2px 8px", "borderRadius": "10px"}},
{"widgettype": "Text", "options": {"text": f" {h['id'][:16]}", "cfontsize": 11, "color": "#999", "marginLeft": "8px"}}
]},
{"widgettype": "Text", "options": {"text": h["text"][:300], "cfontsize": 13, "color": "#333", "lineHeight": "1.6"}}
]
})
return json.dumps({"widgettype": "VBox", "options": {"padding": "20px", "spacing": "4px"}, "subwidgets": subwidgets}, ensure_ascii=False, default=str)