feat(rag): 多媒体知识库接回——qwen3-vl在线(2560维)+CLIP保留给GPU;VDB地址读upapp;在线重排;配置界面4引擎
This commit is contained in:
parent
576f0b6368
commit
03dbf0f2b5
38
rag/init.py
38
rag/init.py
@ -41,10 +41,11 @@ async def kb_list_handler(request, params_kw, *args, **kwargs):
|
||||
size_str = f"{total_size/1024:.0f}KB"
|
||||
else:
|
||||
size_str = f"{total_size}B"
|
||||
engine = {'bge-m3': '文本 bge-m3', 'clip-vith14': '多媒体 CLIP', 'CLIP ViT-H-14': '多媒体 CLIP'}.get((r.embedding_engine or '').strip(), (r.embedding_engine or 'CLIP'))
|
||||
engine = {'bge-m3': '文本·在线', 'qwen3-vl-embedding': '多媒体·在线', 'clip-vith14': '多媒体·GPU CLIP', 'CLIP ViT-H-14': '多媒体·GPU CLIP'}.get((r.embedding_engine or '').strip(), (r.embedding_engine or '未配置'))
|
||||
dim = 2560 if (r.embedding_engine or '').strip() == 'qwen3-vl-embedding' else 1024
|
||||
card = {"widgettype":"VBox","options":{"cwidth":16,"cheight":12,"bgcolor":"#f0f7ff","padding":"16px","css":"card clickable","border":"1px solid #d0e4f7"},"subwidgets":[
|
||||
{"widgettype":"Text","options":{"text":"📚 " + str(r.name),"cfontsize":16,"fontWeight":"bold"}},
|
||||
{"widgettype":"Text","options":{"text":str(engine)+" · 1024维","cfontsize":12,"color":"#888"}},
|
||||
{"widgettype":"Text","options":{"text":str(engine)+f" · {dim}维","cfontsize":12,"color":"#888"}},
|
||||
{"widgettype":"HBox","options":{"spacing":"12px"},"subwidgets":[
|
||||
{"widgettype":"Text","options":{"text":"📄 "+str(doc_count)+"文档","cfontsize":12,"color":"#666"}},
|
||||
{"widgettype":"Text","options":{"text":"💾 "+size_str,"cfontsize":12,"color":"#666"}}]}],
|
||||
@ -992,7 +993,7 @@ async def engine_cfg_get_handler(request, params_kw, *args, **kwargs):
|
||||
async with get_sor_context(env, 'rag') as sor:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT engine_type, model_name, endpoint_url, api_key, status FROM rag_engine_configs "
|
||||
"WHERE engine_type IN ('embedding','rerank') ORDER BY engine_type", {})
|
||||
"WHERE engine_type IN ('embedding','rerank','mm_embedding','mm_rerank') ORDER BY engine_type", {})
|
||||
rows = []
|
||||
for r in recs:
|
||||
enc = getattr(r, "api_key", "") or ""
|
||||
@ -1010,8 +1011,8 @@ async def engine_cfg_save_handler(request, params_kw, *args, **kwargs):
|
||||
env = request._run_ns
|
||||
try:
|
||||
engine_type = (params_kw.get("engine_type") or "").strip()
|
||||
if engine_type not in ("embedding", "rerank"):
|
||||
return json.dumps({"error": "engine_type must be embedding/rerank"})
|
||||
if engine_type not in ("embedding", "rerank", "mm_embedding", "mm_rerank"):
|
||||
return json.dumps({"error": "engine_type must be embedding/rerank/mm_embedding/mm_rerank"})
|
||||
model_id = (params_kw.get("model_id") or "").strip()
|
||||
api_base = (params_kw.get("api_base") or "").strip()
|
||||
api_key_plain = (params_kw.get("api_key") or "").strip()
|
||||
@ -1071,6 +1072,33 @@ async def engine_cfg_test_handler(request, params_kw, *args, **kwargs):
|
||||
import aiohttp
|
||||
headers = {"Authorization": "Bearer " + api_key_plain, "Content-Type": "application/json"}
|
||||
base = api_base.rstrip("/")
|
||||
# 在线多模态:DashScope 原生 API
|
||||
if engine_type == "mm_embedding":
|
||||
if "/api/v1" not in base:
|
||||
base = base + "/api/v1"
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as s:
|
||||
async with s.post(base + "/services/embeddings/multimodal-embedding/multimodal-embedding",
|
||||
json={"model": model_id, "input": {"contents": [{"text": "连通性测试"}]}},
|
||||
headers=headers) as resp:
|
||||
data = await resp.json()
|
||||
embs = (data.get("output") or {}).get("embeddings") or []
|
||||
dim = len(embs[0]["embedding"]) if embs and embs[0].get("embedding") else 0
|
||||
if not dim:
|
||||
return json.dumps({"error": str(data)[:200]}, ensure_ascii=False)
|
||||
return json.dumps({"status": "SUCCEEDED", "message": f"mm_embedding OK,维度 {dim}"}, ensure_ascii=False)
|
||||
if engine_type == "mm_rerank":
|
||||
if "/api/v1" not in base:
|
||||
base = base + "/api/v1"
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as s:
|
||||
async with s.post(base + "/services/rerank/text-rerank/text-rerank",
|
||||
json={"model": model_id,
|
||||
"input": {"query": "测试", "documents": ["甲", "乙"]},
|
||||
"parameters": {"top_n": 2}},
|
||||
headers=headers) as resp:
|
||||
data = await resp.json()
|
||||
if not (data.get("output") or {}).get("results"):
|
||||
return json.dumps({"error": str(data)[:200]}, ensure_ascii=False)
|
||||
return json.dumps({"status": "SUCCEEDED", "message": "mm_rerank OK"}, ensure_ascii=False)
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=20)) as s:
|
||||
if engine_type == "embedding":
|
||||
async with s.post(base + "/embeddings", json={"model": model_id, "input": ["连通性测试"]},
|
||||
|
||||
153
rag/vl_online.py
Normal file
153
rag/vl_online.py
Normal file
@ -0,0 +1,153 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
"""在线多模态引擎:qwen3-vl-embedding / qwen3-vl-rerank(阿里 DashScope 原生 API)。
|
||||
与 CLIP(GPU 本地)并存:kb.embedding_engine=qwen3-vl-embedding 走在线,
|
||||
clip-vith14 仍走 GPU /mme 端点(有 GPU 的环境保留本地部署)。
|
||||
配置来自 rag_engine_configs:engine_type=mm_embedding / mm_rerank,空=能力屏蔽。
|
||||
"""
|
||||
import json
|
||||
from traceback import format_exc
|
||||
from ahserver.serverenv import ServerEnv
|
||||
from appPublic.log import exception
|
||||
from sqlor.dbpools import get_sor_context
|
||||
|
||||
NATIVE_EMB_PATH = "/api/v1/services/embeddings/multimodal-embedding/multimodal-embedding"
|
||||
NATIVE_RR_PATH = "/api/v1/services/rerank/text-rerank/text-rerank"
|
||||
DEFAULT_MM_DIM = 2560
|
||||
TEXT_DIM = 1024
|
||||
|
||||
|
||||
async def get_mm_cfg(engine_type="mm_embedding"):
|
||||
"""读引擎配置,返回 {model_id, api_base, api_key 明文, dim};未配置/空 → None(能力屏蔽)。"""
|
||||
from appPublic.rc4 import unpassword
|
||||
env = ServerEnv()
|
||||
try:
|
||||
async with get_sor_context(env, 'rag') as sor:
|
||||
recs = await sor.sqlExe(
|
||||
"SELECT model_name, endpoint_url, api_key, config_json FROM rag_engine_configs "
|
||||
"WHERE engine_type=${t}$ AND status='active' ORDER BY is_default DESC, priority DESC LIMIT 1",
|
||||
{"t": engine_type})
|
||||
if not recs:
|
||||
return None
|
||||
r = recs[0]
|
||||
model_id = (getattr(r, "model_name", "") or "").strip()
|
||||
api_base = (getattr(r, "endpoint_url", "") or "").strip()
|
||||
enc = (getattr(r, "api_key", "") or "").strip()
|
||||
if not (model_id and api_base and enc):
|
||||
return None
|
||||
try:
|
||||
from appPublic.jsonConfig import getConfig
|
||||
key = getConfig().password_key or 'QRIVSRHrthhwyjy176556332'
|
||||
api_key = unpassword(enc, key)
|
||||
except Exception:
|
||||
api_key = enc
|
||||
dim = DEFAULT_MM_DIM
|
||||
try:
|
||||
cj = json.loads(getattr(r, "config_json", "") or "{}")
|
||||
dim = int(cj.get("dim", DEFAULT_MM_DIM))
|
||||
except Exception:
|
||||
pass
|
||||
return {"model_id": model_id, "api_base": api_base.rstrip("/"), "api_key": api_key, "dim": dim}
|
||||
except Exception as e:
|
||||
exception(f"mm cfg read failed ({engine_type}): {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def _post_json(url, payload, headers, timeout=60):
|
||||
import aiohttp
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout)) as s:
|
||||
async with s.post(url, json=payload, headers=headers) as resp:
|
||||
return await resp.json()
|
||||
|
||||
|
||||
async def vl_embed_contents(contents, engine_type="mm_embedding"):
|
||||
"""单条多模态向量化。contents 例:[{"text": "..."}] 或 [{"image": b64dataurl}] 或图文混合。"""
|
||||
cfg = await get_mm_cfg(engine_type)
|
||||
if not cfg:
|
||||
return None
|
||||
url = cfg["api_base"] + NATIVE_EMB_PATH
|
||||
headers = {"Authorization": "Bearer " + cfg["api_key"], "Content-Type": "application/json"}
|
||||
try:
|
||||
data = await _post_json(url, {"model": cfg["model_id"], "input": {"contents": contents}}, headers)
|
||||
embs = (data.get("output") or {}).get("embeddings") or []
|
||||
return embs[0]["embedding"] if embs and embs[0].get("embedding") else None
|
||||
except Exception as e:
|
||||
exception(f"vl embed failed: {e}, {format_exc()}")
|
||||
return None
|
||||
|
||||
|
||||
async def vl_embed_texts(texts, engine_type="mm_embedding"):
|
||||
"""批量纯文本向量化(走 vl 原生 API,contents=[{"text":t}])。"""
|
||||
cfg = await get_mm_cfg(engine_type)
|
||||
if not cfg:
|
||||
return []
|
||||
url = cfg["api_base"] + NATIVE_EMB_PATH
|
||||
headers = {"Authorization": "Bearer " + cfg["api_key"], "Content-Type": "application/json"}
|
||||
out = []
|
||||
for t in texts:
|
||||
try:
|
||||
data = await _post_json(url, {"model": cfg["model_id"], "input": {"contents": [{"text": t}]}}, headers)
|
||||
embs = (data.get("output") or {}).get("embeddings") or []
|
||||
out.append(embs[0]["embedding"] if embs and embs[0].get("embedding") else None)
|
||||
except Exception as e:
|
||||
exception(f"vl embed text failed: {e}")
|
||||
out.append(None)
|
||||
return out
|
||||
|
||||
|
||||
async def vl_embed_image_bytes(image_bytes, engine_type="mm_embedding"):
|
||||
"""图片 bytes → base64 data url → 在线向量化。"""
|
||||
import base64
|
||||
b64 = base64.b64encode(image_bytes).decode()
|
||||
return await vl_embed_contents([{"image": "data:image/jpeg;base64," + b64}], engine_type)
|
||||
|
||||
|
||||
async def vl_rerank(query, documents):
|
||||
"""在线多模态重排。query: 文本 str;documents: list[str] 或 [{\"image\": ...}]。
|
||||
未配置/失败返回 None(调用方回退召回分排序)。"""
|
||||
cfg = await get_mm_cfg("mm_rerank")
|
||||
if not cfg:
|
||||
return None
|
||||
base = cfg["api_base"]
|
||||
if "compatible" in base:
|
||||
base = base.replace("/compatible-mode", "").replace("/compatible-api", "")
|
||||
if "/api/v1" not in base:
|
||||
base = base + "/api/v1"
|
||||
url = base + NATIVE_RR_PATH.replace("/api/v1", "")
|
||||
headers = {"Authorization": "Bearer " + cfg["api_key"], "Content-Type": "application/json"}
|
||||
payload = {"model": cfg["model_id"],
|
||||
"input": {"query": query, "documents": documents},
|
||||
"parameters": {"top_n": len(documents), "return_documents": False}}
|
||||
try:
|
||||
data = await _post_json(url, payload, headers, timeout=30)
|
||||
results = (data.get("output") or {}).get("results") or []
|
||||
if not results:
|
||||
return None
|
||||
scores = [0.0] * len(documents)
|
||||
for it in results:
|
||||
idx = it.get("index")
|
||||
if isinstance(idx, int) and 0 <= idx < len(scores):
|
||||
scores[idx] = it.get("relevance_score", 0)
|
||||
return {"scores": scores}
|
||||
except Exception as e:
|
||||
exception(f"vl rerank failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def vdb_baseurl():
|
||||
"""VDB 服务地址:读 upapp.rag-vdb(生产已切内网 9186),不再硬编码域名。"""
|
||||
env = ServerEnv()
|
||||
async with get_sor_context(env, 'rag') as sor:
|
||||
recs = await sor.sqlExe("SELECT baseurl FROM upapp WHERE id='rag-vdb'", {})
|
||||
if not recs:
|
||||
raise Exception("upapp rag-vdb not configured")
|
||||
return (recs[0].baseurl or "").rstrip("/")
|
||||
|
||||
|
||||
async def vdb_call(apiname, payload, timeout=20):
|
||||
"""直调 VDB HTTP 接口(createcollection/upsert),URL 来自 upapp。"""
|
||||
import aiohttp
|
||||
base = await vdb_baseurl()
|
||||
url = f"{base}/v1/{apiname}"
|
||||
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout)) as s:
|
||||
async with s.post(url, json=payload, headers={"Content-Type": "application/json"}) as resp:
|
||||
return await resp.json()
|
||||
@ -14,6 +14,8 @@ async def get_cfg(t):
|
||||
|
||||
emb = await get_cfg("embedding")
|
||||
rr = await get_cfg("rerank")
|
||||
mm_emb = await get_cfg("mm_embedding")
|
||||
mm_rr = await get_cfg("mm_rerank")
|
||||
|
||||
def card(title, desc, prefix, cfg):
|
||||
shielded = (not cfg["model_id"]) or cfg["status"] != "active"
|
||||
@ -56,7 +58,9 @@ def card(title, desc, prefix, cfg):
|
||||
}
|
||||
|
||||
result = {"widgettype": "VBox", "options": {"width": "100%", "spacing": "16px"}, "subwidgets": [
|
||||
card("🔤 文本向量化(Embedding)", "文本知识库必需。模型留空或停用 = 屏蔽(文档仍入库可浏览,检索不可用)。", "embedding", emb),
|
||||
card("🎯 重排序(Rerank)", "检索结果精排,可选。未配置时按召回分排序。", "rerank", rr)
|
||||
card("🔤 文本向量化(Embedding)", "文本知识库(bge-m3引擎)必需。模型留空或停用 = 屏蔽(文档仍入库可浏览,检索不可用)。", "embedding", emb),
|
||||
card("🎯 文本重排(Rerank)", "文本检索结果精排,可选。未配置时按召回分排序。", "rerank", rr),
|
||||
card("🖼️ 多模态向量化(qwen3-vl)", "多媒体知识库必需(图文跨模态)。支持以文搜图/以图搜文。留空=屏蔽多媒体检索。", "mm_embedding", mm_emb),
|
||||
card("🖼️ 多模态重排(qwen3-vl-rerank)", "多媒体/混合检索精排,可选。未配置时按召回分排序。", "mm_rerank", mm_rr)
|
||||
]}
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
|
||||
@ -7,10 +7,10 @@ async with db.sqlorContext(dbname) as sor:
|
||||
kb_id = uuid()
|
||||
name = ns.get("name", "")
|
||||
desc = ns.get("description", "")
|
||||
# 向量引擎:clip-vith14=多媒体(CLIP),bge-m3=文本(bge-m3)。默认 clip-vith14 兼容旧数据
|
||||
# 向量引擎:bge-m3=文本(在线) / qwen3-vl-embedding=多媒体(在线) / clip-vith14=多媒体(GPU本地CLIP)
|
||||
emb_type = ns.get("embedding_type", "") or ns.get("embedding_engine", "")
|
||||
if emb_type not in ("bge-m3", "clip-vith14"):
|
||||
emb_type = "clip-vith14"
|
||||
if emb_type not in ("bge-m3", "clip-vith14", "qwen3-vl-embedding"):
|
||||
emb_type = "bge-m3"
|
||||
await sor.sqlExe(
|
||||
"INSERT INTO rag_knowledge_bases (id, name, description, org_id, embedding_engine, vdb_collection, doc_count, total_size, chunk_count, status, created_at) "
|
||||
"VALUES (${id}$, ${name}$, ${desc}$, ${org_id}$, ${emb}$, 'rag_collection', 0, 0, 0, 'active', NOW())",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import json
|
||||
return json.dumps([
|
||||
{"value": "clip-vith14", "text": "多媒体(图文音视)"},
|
||||
{"value": "bge-m3", "text": "文本(文档检索)"}
|
||||
{"value": "bge-m3", "text": "文本(在线·文档检索)"},
|
||||
{"value": "qwen3-vl-embedding", "text": "多媒体(在线·图文跨模态)"},
|
||||
{"value": "clip-vith14", "text": "多媒体(GPU本地CLIP·需GPU)"}
|
||||
], ensure_ascii=False)
|
||||
|
||||
@ -75,7 +75,7 @@ hits = []
|
||||
raw_rows = []
|
||||
kw_rows = []
|
||||
|
||||
# 读知识库向量引擎,决定用文本(bge-m3 /txte)还是多媒体(CLIP /mme) embedding
|
||||
# 读知识库向量引擎:bge-m3(在线文本) / qwen3-vl-embedding(在线多模态) / clip-vith14(GPU本地)
|
||||
emb_engine = 'clip-vith14'
|
||||
try:
|
||||
async with get_sor_context(env, 'rag') as sor:
|
||||
@ -83,6 +83,7 @@ try:
|
||||
if krecs:
|
||||
emb_engine = (getattr(krecs[0], 'embedding_engine', '') or 'clip-vith14').strip()
|
||||
except: pass
|
||||
is_vl = emb_engine == 'qwen3-vl-embedding'
|
||||
if emb_engine == 'bge-m3':
|
||||
emb_url = 'https://embedding.opencomputing.net:10443/txte/api/embed'
|
||||
emb_model = 'bge-m3'
|
||||
@ -90,25 +91,47 @@ else:
|
||||
emb_url = 'https://embedding.opencomputing.net:10443/mme/api/embed'
|
||||
emb_model = 'CLIP-ViT-H-14'
|
||||
|
||||
# VDB 服务地址:读 upapp.rag-vdb(生产已切内网),不硬编码
|
||||
try:
|
||||
async with get_sor_context(env, 'rag') as sor:
|
||||
_u = await sor.sqlExe("SELECT baseurl FROM upapp WHERE id='rag-vdb'", {})
|
||||
VDB_BASE = (_u[0].baseurl or '').rstrip('/') if _u else 'https://vectordb.opencomputing.net:10443'
|
||||
except:
|
||||
VDB_BASE = 'https://vectordb.opencomputing.net:10443'
|
||||
|
||||
if query:
|
||||
# 1. Embed
|
||||
# 1. Embed(在线:vl原生 / bge-m3兼容;GPU:CLIP /mme)
|
||||
vec = []
|
||||
try:
|
||||
client = StreamHttpClient()
|
||||
resp = await client.request('POST', emb_url,
|
||||
json={"texts": [query], "model": emb_model})
|
||||
emb = json.loads(resp)
|
||||
vec = emb.get("text_embeddings", emb.get("embeddings", [[]]))[0]
|
||||
except:
|
||||
pass
|
||||
if is_vl:
|
||||
try:
|
||||
from rag.vl_online import vl_embed_contents
|
||||
vec = await vl_embed_contents([{"text": query}]) or []
|
||||
except:
|
||||
vec = []
|
||||
elif emb_engine == 'bge-m3':
|
||||
try:
|
||||
from rag.init import _online_embed
|
||||
_vs = await _online_embed(env, [query])
|
||||
vec = _vs[0] if _vs else []
|
||||
except:
|
||||
vec = []
|
||||
else:
|
||||
try:
|
||||
client = StreamHttpClient()
|
||||
resp = await client.request('POST', emb_url,
|
||||
json={"texts": [query], "model": emb_model})
|
||||
emb = json.loads(resp)
|
||||
vec = emb.get("text_embeddings", emb.get("embeddings", [[]]))[0]
|
||||
except:
|
||||
pass
|
||||
if not vec:
|
||||
return json.dumps({"widgettype": "Text", "options": {"text": "向量化失败", "cfontsize": 14, "color": "#e74c3c"}}, ensure_ascii=False)
|
||||
return json.dumps({"widgettype": "Text", "options": {"text": "向量化失败(引擎未配置或不可用,请到引擎配置页检查)", "cfontsize": 14, "color": "#e74c3c"}}, ensure_ascii=False)
|
||||
|
||||
# 2. VDB search
|
||||
try:
|
||||
recall_n = top_k * 3
|
||||
client2 = StreamHttpClient()
|
||||
resp2 = await client2.request('POST', 'https://vectordb.opencomputing.net:10443/v1/query',
|
||||
resp2 = await client2.request('POST', VDB_BASE + '/v1/query',
|
||||
json={"colname": kb_id, "vector": vec, "pagerows": recall_n, "output_fields": ["*"]})
|
||||
vdb = json.loads(resp2)
|
||||
raw_rows = vdb.get("data", {}).get("rows", [])
|
||||
@ -172,7 +195,23 @@ if query:
|
||||
hits.append(kr)
|
||||
seen.add(kr["id"])
|
||||
|
||||
hits.sort(key=lambda x: x.get("score", 0), reverse=True)
|
||||
# 在线重排(mm_rerank;未配置/失败则保持召回分排序)
|
||||
rr_applied = False
|
||||
try:
|
||||
from rag.vl_online import vl_rerank
|
||||
cand = hits[:top_k * 3]
|
||||
if cand:
|
||||
rr = await vl_rerank(query, [h.get("text", "") for h in cand])
|
||||
if rr and isinstance(rr.get("scores"), list) and len(rr["scores"]) == len(cand):
|
||||
for i, s in enumerate(rr["scores"]):
|
||||
cand[i]["score"] = s
|
||||
cand.sort(key=lambda x: x.get("score", 0), reverse=True)
|
||||
rr_applied = True
|
||||
except Exception as e:
|
||||
info('[search_result] rerank failed: %s' % e)
|
||||
|
||||
if not rr_applied:
|
||||
hits.sort(key=lambda x: x.get("score", 0), reverse=True)
|
||||
hits = hits[:top_k]
|
||||
|
||||
else:
|
||||
|
||||
@ -51,7 +51,10 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
|
||||
# 后台任务无 request/env,用注入的全局函数解析;禁硬编码库名
|
||||
_dbname = get_module_dbname('rag')
|
||||
|
||||
# 读知识库向量引擎:bge-m3=文本(走 /txte),clip-vith14=多媒体(走 /mme)
|
||||
# 读知识库向量引擎:
|
||||
# bge-m3 → 在线文本(阿里, 1024维)
|
||||
# qwen3-vl-embedding → 在线多模态(阿里, 2560维)
|
||||
# clip-vith14 → GPU 本地 CLIP(1024维, 有GPU环境保留)
|
||||
emb_engine = 'clip-vith14'
|
||||
try:
|
||||
async with db.sqlorContext(_dbname) as sor:
|
||||
@ -60,6 +63,17 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
|
||||
emb_engine = (getattr(krecs[0], 'embedding_engine', '') or 'clip-vith14').strip()
|
||||
except: pass
|
||||
is_text = emb_engine == 'bge-m3'
|
||||
is_vl = emb_engine == 'qwen3-vl-embedding'
|
||||
vdb_dim = 2560 if is_vl else 1024
|
||||
|
||||
# VDB 服务地址:读 upapp.rag-vdb(生产已切内网),不硬编码
|
||||
try:
|
||||
async with db.sqlorContext(_dbname) as sor:
|
||||
_u = await sor.sqlExe("SELECT baseurl FROM upapp WHERE id='rag-vdb'", {})
|
||||
VDB_BASE = (_u[0].baseurl or '').rstrip('/') if _u else 'https://vectordb.opencomputing.net:10443'
|
||||
except:
|
||||
VDB_BASE = 'https://vectordb.opencomputing.net:10443'
|
||||
|
||||
if is_text:
|
||||
emb_url = 'https://embedding.opencomputing.net:10443/txte/api/embed'
|
||||
emb_model = 'bge-m3'
|
||||
@ -67,9 +81,49 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
|
||||
emb_url = 'https://embedding.opencomputing.net:10443/mme/api/embed'
|
||||
emb_model = 'CLIP-ViT-H-14'
|
||||
|
||||
# 在线引擎:配置读取 + key 解密(mm_embedding / mm_rerank)
|
||||
_vl_cfg = None
|
||||
if is_vl or not is_text:
|
||||
try:
|
||||
from rag.vl_online import get_mm_cfg
|
||||
_vl_cfg = await get_mm_cfg('mm_embedding')
|
||||
except:
|
||||
_vl_cfg = None
|
||||
|
||||
async def _online_text_embed(texts):
|
||||
"""文本向量化:vl 引擎走在线原生API;bge-m3 走在线兼容API(经 engine_configs)"""
|
||||
if is_vl and _vl_cfg:
|
||||
from rag.vl_online import vl_embed_texts
|
||||
return await vl_embed_texts(texts)
|
||||
try:
|
||||
from rag.init import _online_embed
|
||||
from ahserver.serverenv import ServerEnv
|
||||
return await _online_embed(ServerEnv(), texts)
|
||||
except:
|
||||
return []
|
||||
|
||||
async def _media_embed_image(img_b64):
|
||||
"""图片向量化:vl 在线(base64) 或 GPU CLIP"""
|
||||
if is_vl and _vl_cfg:
|
||||
try:
|
||||
from rag.vl_online import vl_embed_contents
|
||||
v = await vl_embed_contents([{"image": "data:image/jpeg;base64," + img_b64}])
|
||||
return v
|
||||
except:
|
||||
return None
|
||||
try:
|
||||
client = StreamHttpClient()
|
||||
resp = await client.request('POST', emb_url,
|
||||
json={"images": [img_b64], "model": emb_model})
|
||||
emb_data = json.loads(resp)
|
||||
es = emb_data.get("image_embeddings", emb_data.get("embeddings", []))
|
||||
return es[0] if es else None
|
||||
except:
|
||||
return None
|
||||
|
||||
async def ensure_vdb_collection(client, kb_id):
|
||||
payload = {"colname": kb_id, "fields": [{"name": "id", "type": "str", "is_primary": True, "max_length": 64}, {"name": "vector", "type": "fvector", "dim": 1024}, {"name": "text", "type": "str", "max_length": 65535}], "description": "RAG kb", "metric": "COSINE"}
|
||||
await client.request('POST', 'https://vectordb.opencomputing.net:10443/v1/createcollection', json=payload)
|
||||
payload = {"colname": kb_id, "fields": [{"name": "id", "type": "str", "is_primary": True, "max_length": 64}, {"name": "vector", "type": "fvector", "dim": vdb_dim}, {"name": "text", "type": "str", "max_length": 65535}], "description": "RAG kb", "metric": "COSINE"}
|
||||
await client.request('POST', VDB_BASE + '/v1/createcollection', json=payload)
|
||||
|
||||
text = ''
|
||||
chunks_n = 0
|
||||
@ -126,7 +180,7 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
|
||||
except: pass
|
||||
return
|
||||
|
||||
# --- IMAGE: face detection ---
|
||||
# --- IMAGE: face detection + 图片向量化入库 ---
|
||||
if ext_l in image_exts:
|
||||
img_b64 = base64.b64encode(file_data).decode()
|
||||
try:
|
||||
@ -141,6 +195,24 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
|
||||
meta_parts['face_bboxes'] = [f.get("bbox", {}) for f in faces[:10]]
|
||||
meta_parts['face'] = face_count
|
||||
except: pass
|
||||
# 图片本身向量化(vl在线 或 GPU CLIP),存入 VDB 供以文搜图
|
||||
try:
|
||||
img_vec = await _media_embed_image(img_b64)
|
||||
if img_vec:
|
||||
client2 = StreamHttpClient()
|
||||
await ensure_vdb_collection(client2, kb_id)
|
||||
vdb_data = {"colname": kb_id, "data": [
|
||||
{"id": doc_id + "_c0", "vector": img_vec, "text": file_name}]}
|
||||
await client2.request('POST', VDB_BASE + '/v1/upsert', json=vdb_data)
|
||||
async with db.sqlorContext(_dbname) as sor:
|
||||
await sor.sqlExe(
|
||||
"INSERT INTO rag_document_chunks (id, doc_id, kb_id, chunk_index, content, vector_id, created_at) "
|
||||
"VALUES (${id}$, ${doc_id}$, ${kb_id}$, 0, ${content}$, ${vid}$, NOW())",
|
||||
{"id": doc_id + "_c0", "doc_id": doc_id, "kb_id": kb_id,
|
||||
"content": file_name, "vid": doc_id + "_c0"})
|
||||
chunks_n = 1
|
||||
meta_parts['image'] = 'embedded'
|
||||
except: pass
|
||||
|
||||
# --- AUDIO: voiceprint ---
|
||||
if ext_l in audio_exts:
|
||||
@ -176,13 +248,10 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
|
||||
face_count = len(faces)
|
||||
frame_bboxes = [f.get("bbox", {}) for f in faces[:10]] if faces else []
|
||||
except: pass
|
||||
# --- CLIP image embedding for video frame ---
|
||||
# --- 视频帧向量化(vl在线 或 GPU CLIP) ---
|
||||
try:
|
||||
client2 = StreamHttpClient()
|
||||
resp2 = await client2.request('POST', emb_url,
|
||||
json={"images": [img_b64], "model": emb_model})
|
||||
emb_data = json.loads(resp2)
|
||||
img_embeddings = emb_data.get("image_embeddings", emb_data.get("embeddings", []))
|
||||
frame_vec = await _media_embed_image(img_b64)
|
||||
img_embeddings = [frame_vec] if frame_vec else []
|
||||
except:
|
||||
img_embeddings = []
|
||||
if img_embeddings:
|
||||
@ -192,7 +261,7 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
|
||||
vdb_data = {"colname": kb_id, "data": [
|
||||
{"id": doc_id + "_c0", "vector": img_embeddings[0], "text": file_name}
|
||||
]}
|
||||
await client3.request('POST', 'https://vectordb.opencomputing.net:10443/v1/upsert', json=vdb_data)
|
||||
await client3.request('POST', VDB_BASE + '/v1/upsert', json=vdb_data)
|
||||
chunk_meta = {"start_time": 0}
|
||||
if frame_bboxes:
|
||||
chunk_meta["bboxes"] = frame_bboxes
|
||||
@ -253,14 +322,9 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
|
||||
if cur: chunks.append(cur)
|
||||
|
||||
if chunks:
|
||||
try:
|
||||
client = StreamHttpClient()
|
||||
resp = await client.request('POST', emb_url,
|
||||
json={"texts": chunks, "model": emb_model})
|
||||
emb_data = json.loads(resp)
|
||||
embeddings = emb_data.get("text_embeddings", emb_data.get("embeddings", []))
|
||||
except:
|
||||
embeddings = []
|
||||
# 在线文本向量化(vl引擎→qwen3-vl原生;bge-m3→在线兼容;GPU CLIP 时代的老路径已移除)
|
||||
embeddings = await _online_text_embed(chunks)
|
||||
embeddings = [e for e in embeddings if e] or []
|
||||
|
||||
vector_ids = []
|
||||
if embeddings:
|
||||
@ -270,7 +334,7 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
|
||||
vdb_data = {"colname": kb_id, "data": [
|
||||
{"id": doc_id + "_c" + str(i), "vector": emb, "text": chunks[i]}
|
||||
for i, emb in enumerate(embeddings)]}
|
||||
resp3 = await client2.request('POST', 'https://vectordb.opencomputing.net:10443/v1/upsert', json=vdb_data)
|
||||
resp3 = await client2.request('POST', VDB_BASE + '/v1/upsert', json=vdb_data)
|
||||
if json.loads(resp3).get('status') == 'SUCCEEDED':
|
||||
vector_ids = [doc_id + "_c" + str(i) for i in range(len(embeddings))]
|
||||
except:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user