107 lines
5.4 KiB
Plaintext
107 lines
5.4 KiB
Plaintext
"""搜索 DSPY — 接收 Form 参数,返回搜索结果 Widget"""
|
|
query = (params_kw.get("query") or "").strip()
|
|
kb_id = (params_kw.get("kb_id") or "").strip()
|
|
top_k = int(params_kw.get("top_k") or 10)
|
|
recall_k = int(params_kw.get("recall_k") or top_k * 3)
|
|
env = request._run_ns
|
|
userorgid = await env.get_userorgid()
|
|
|
|
# Resolve KBs
|
|
async with get_sor_context(env, 'rag') as sor:
|
|
if kb_id:
|
|
recs = await sor.R("knowledge_bases", {"id": kb_id})
|
|
kb_ids = [r.id for r in recs]
|
|
else:
|
|
sql = "SELECT id FROM knowledge_bases WHERE org_id IS NULL OR org_id=${org_id}$"
|
|
recs = await sor.sqlExe(sql, {"org_id": userorgid})
|
|
kb_ids = [r.id for r in recs]
|
|
|
|
if not kb_ids:
|
|
result = {"widgettype": "Text", "options": {"text": "没有可搜索的知识库", "color": "#aaa", "halign": "center", "padding": "40px", "cfontsize": 14}}
|
|
|
|
elif not query:
|
|
result = {"widgettype": "Text", "options": {"text": "请输入搜索关键词", "color": "#aaa", "halign": "center", "padding": "40px", "cfontsize": 14}}
|
|
|
|
else:
|
|
# Build embedding
|
|
from rag.init import _call_uapi
|
|
try:
|
|
emb_resp = await _call_uapi("rag-embedding", "embed", {"texts": [query], "model": "CLIP-ViT-H-14"})
|
|
query_vec = (emb_resp.get("embeddings", []) or [None])[0] if isinstance(emb_resp, dict) else None
|
|
except Exception:
|
|
query_vec = None
|
|
|
|
if not query_vec:
|
|
result = {"widgettype": "Text", "options": {"text": "向量化失败,请稍后重试", "color": "#e53e3e", "halign": "center", "padding": "40px", "cfontsize": 14}}
|
|
else:
|
|
# Multi-KB VDB search
|
|
all_hits = []
|
|
for kid in kb_ids:
|
|
try:
|
|
vdb_resp = await _call_uapi("rag-vdb", "search", {"collection": kid, "vector": query_vec, "topK": recall_k})
|
|
data = vdb_resp
|
|
if isinstance(data, dict):
|
|
for key in ("results", "data", "hits", "rows"):
|
|
if isinstance(data.get(key), list):
|
|
data = data[key]
|
|
break
|
|
if isinstance(data, list):
|
|
for item in data:
|
|
if isinstance(item, dict):
|
|
all_hits.append({"id": item.get("id", ""), "text": item.get("text", item.get("content", "")),
|
|
"score": item.get("score", item.get("distance", 0)), "kb_id": kid})
|
|
except Exception:
|
|
pass
|
|
|
|
# Deduplicate + sort
|
|
seen = set()
|
|
unique_hits = []
|
|
for h in sorted(all_hits, key=lambda x: x.get("score", 0), reverse=True):
|
|
hid = h.get("id", h.get("text", ""))
|
|
if hid not in seen:
|
|
seen.add(hid)
|
|
unique_hits.append(h)
|
|
|
|
# Rerank
|
|
if query and unique_hits:
|
|
docs = [h.get("text", "")[:500] for h in unique_hits[:recall_k]]
|
|
try:
|
|
rr = await _call_uapi("rag-reranker", "rerank", {"query": query, "documents": docs})
|
|
scores = rr.get("scores", rr.get("results", [])) if isinstance(rr, dict) else []
|
|
if isinstance(scores, list) and len(scores) == len(docs):
|
|
for i, s in enumerate(scores):
|
|
unique_hits[i]["rerank_score"] = s.get("score", 0) if isinstance(s, dict) else float(s or 0)
|
|
unique_hits.sort(key=lambda x: x.get("rerank_score", 0), reverse=True)
|
|
except Exception:
|
|
pass
|
|
|
|
final = unique_hits[:top_k]
|
|
|
|
if not final:
|
|
result = {"widgettype": "Text", "options": {"text": "未找到相关结果", "color": "#aaa", "halign": "center", "padding": "40px", "cfontsize": 14}}
|
|
else:
|
|
cards = []
|
|
for i, h in enumerate(final):
|
|
text = h.get("text", "")[:300]
|
|
score = h.get("rerank_score", h.get("score", 0))
|
|
score_pct = f"{min(abs(float(score)) * 100, 100):.0f}%" if score else ""
|
|
cards.append({
|
|
"widgettype": "VBox",
|
|
"options": {"cwidth": 100, "padding": "16px", "bgcolor": "#f8fafc", "border": "1px solid #e2e8f0", "borderRadius": "8px", "spacing": "8px"},
|
|
"subwidgets": [
|
|
{"widgettype": "HBox", "options": {"spacing": "8px"}, "subwidgets": [
|
|
{"widgettype": "Text", "options": {"text": f"#{i+1}", "cfontsize": 12, "color": "#3b82f6", "fontWeight": "bold", "padding": "2px 8px", "bgcolor": "#eff6ff", "borderRadius": "4px"}},
|
|
{"widgettype": "Text", "options": {"text": f"相关度: {score_pct}", "cfontsize": 11, "color": "#888"}},
|
|
{"widgettype": "Text", "options": {"text": f"KB: {h.get('kb_id','')[:12]}", "cfontsize": 11, "color": "#aaa"}}
|
|
]},
|
|
{"widgettype": "Text", "options": {"text": text, "cfontsize": 13, "color": "#333", "lineHeight": 1.6}},
|
|
{"widgettype": "Text", "options": {"text": h.get("id", "")[:40], "cfontsize": 10, "color": "#bbb"}}
|
|
]
|
|
})
|
|
result = {"widgettype": "VBox", "options": {"width": "100%", "spacing": "12px"}, "subwidgets": [
|
|
{"widgettype": "Text", "options": {"text": f"找到 {len(final)} 条结果 (共召回 {len(all_hits)} 条)", "cfontsize": 13, "color": "#666", "padding": "0 0 8px 0"}},
|
|
*cards
|
|
]}
|
|
|
|
result
|