feat(embedding): 知识库支持文本(bge-m3 /txte)/多媒体(CLIP /mme)双向量引擎
- create_kb/new_kb_form 加向量引擎选择(文本 bge-m3 / 多媒体 clip-vith14) - upload_file/batch_ingest/search_result/init.py 按 embedding_engine 路由到 /txte 或 /mme - 文本知识库上传媒体文件友好拒绝(不再崩溃报错)
This commit is contained in:
parent
5354108546
commit
1054dbb3a9
49
rag/init.py
49
rag/init.py
@ -113,7 +113,7 @@ async def search_handler(request, params_kw, *args, **kwargs):
|
|||||||
# Process query + media → embedding vector
|
# Process query + media → embedding vector
|
||||||
query_vec = None
|
query_vec = None
|
||||||
if query or file_data:
|
if query or file_data:
|
||||||
query_vec = await _build_search_vector(query, file_data, file_name)
|
query_vec = await _build_search_vector(query, file_data, file_name, env, kb_id)
|
||||||
|
|
||||||
if not query_vec:
|
if not query_vec:
|
||||||
return json.dumps({"status": "SUCCEEDED", "data": {"results": [], "total": 0, "message": "no query or file provided"}}, ensure_ascii=False)
|
return json.dumps({"status": "SUCCEEDED", "data": {"results": [], "total": 0, "message": "no query or file provided"}}, ensure_ascii=False)
|
||||||
@ -181,8 +181,8 @@ async def _resolve_search_kbs(env, userorgid, kb_id):
|
|||||||
return [r.id for r in recs]
|
return [r.id for r in recs]
|
||||||
|
|
||||||
|
|
||||||
async def _build_search_vector(query, file_data, file_name):
|
async def _build_search_vector(query, file_data, file_name, env=None, kb_id=''):
|
||||||
"""Build search embedding from text query + media file"""
|
"""Build search embedding from text query + media file(按 kb 向量引擎选端点)"""
|
||||||
texts = []
|
texts = []
|
||||||
if query:
|
if query:
|
||||||
texts.append(query)
|
texts.append(query)
|
||||||
@ -216,11 +216,23 @@ async def _build_search_vector(query, file_data, file_name):
|
|||||||
|
|
||||||
combined = " ".join(texts)
|
combined = " ".join(texts)
|
||||||
try:
|
try:
|
||||||
resp = await _call_uapi("rag-embedding", "embed", {
|
emb_engine = 'clip-vith14'
|
||||||
"texts": [combined],
|
if env is not None and kb_id:
|
||||||
"model": "CLIP-ViT-H-14"
|
async with get_sor_context(env, 'rag') as sor:
|
||||||
})
|
krecs = await sor.sqlExe("SELECT embedding_engine FROM knowledge_bases WHERE id=${kb_id}$", {"kb_id": kb_id})
|
||||||
vecs = resp.get("embeddings", []) if isinstance(resp, dict) else []
|
if krecs:
|
||||||
|
emb_engine = (getattr(krecs[0], 'embedding_engine', '') or 'clip-vith14').strip()
|
||||||
|
if emb_engine == 'bge-m3':
|
||||||
|
emb_url = 'https://embedding.opencomputing.net:10443/txte/api/embed'
|
||||||
|
emb_model = 'bge-m3'
|
||||||
|
else:
|
||||||
|
emb_url = 'https://embedding.opencomputing.net:10443/mme/api/embed'
|
||||||
|
emb_model = 'CLIP-ViT-H-14'
|
||||||
|
import aiohttp
|
||||||
|
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=15)) as s:
|
||||||
|
async with s.post(emb_url, json={"texts": [combined], "model": emb_model}) as resp:
|
||||||
|
emb_resp = await resp.json()
|
||||||
|
vecs = emb_resp.get("text_embeddings", emb_resp.get("embeddings", [])) if isinstance(emb_resp, dict) else []
|
||||||
return vecs[0] if vecs else None
|
return vecs[0] if vecs else None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
exception(f"query embedding failed: {e}")
|
exception(f"query embedding failed: {e}")
|
||||||
@ -480,11 +492,24 @@ async def _rag_ingest_async(env, text, kb_id, doc_id):
|
|||||||
return {"chunks": 0}
|
return {"chunks": 0}
|
||||||
chunk_count = len(chunks)
|
chunk_count = len(chunks)
|
||||||
|
|
||||||
# 1. Embedding
|
# 1. Embedding(按知识库向量引擎选文本/多模态端点)
|
||||||
try:
|
try:
|
||||||
emb_resp = await _call_uapi("rag-embedding", "embed",
|
emb_engine = 'clip-vith14'
|
||||||
{"texts": chunks, "model": "CLIP-ViT-H-14"})
|
async with get_sor_context(env, 'rag') as sor:
|
||||||
embeddings = emb_resp.get("embeddings", []) if isinstance(emb_resp, dict) else []
|
krecs = await sor.sqlExe("SELECT embedding_engine FROM knowledge_bases WHERE id=${kb_id}$", {"kb_id": kb_id})
|
||||||
|
if krecs:
|
||||||
|
emb_engine = (getattr(krecs[0], 'embedding_engine', '') or 'clip-vith14').strip()
|
||||||
|
if emb_engine == 'bge-m3':
|
||||||
|
emb_url = 'https://embedding.opencomputing.net:10443/txte/api/embed'
|
||||||
|
emb_model = 'bge-m3'
|
||||||
|
else:
|
||||||
|
emb_url = 'https://embedding.opencomputing.net:10443/mme/api/embed'
|
||||||
|
emb_model = 'CLIP-ViT-H-14'
|
||||||
|
import aiohttp
|
||||||
|
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=15)) as s:
|
||||||
|
async with s.post(emb_url, json={"texts": chunks, "model": emb_model}) as resp:
|
||||||
|
emb_resp = await resp.json()
|
||||||
|
embeddings = emb_resp.get("text_embeddings", emb_resp.get("embeddings", [])) if isinstance(emb_resp, dict) else []
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
exception(f"embedding failed: {e}")
|
exception(f"embedding failed: {e}")
|
||||||
embeddings = []
|
embeddings = []
|
||||||
|
|||||||
@ -9,6 +9,14 @@ async with get_sor_context(env, 'rag') as sor:
|
|||||||
"SELECT id, file_name, file_size, file_path FROM documents WHERE kb_id=${kb_id}$ AND (chunk_count IS NULL OR chunk_count=0 OR status='pending') LIMIT 20",
|
"SELECT id, file_name, file_size, file_path FROM documents WHERE kb_id=${kb_id}$ AND (chunk_count IS NULL OR chunk_count=0 OR status='pending') LIMIT 20",
|
||||||
{"kb_id": kb_id})
|
{"kb_id": kb_id})
|
||||||
rows = [dict(r) for r in recs]
|
rows = [dict(r) for r in recs]
|
||||||
|
krecs = await sor.sqlExe("SELECT embedding_engine FROM knowledge_bases WHERE id=${kb_id}$", {"kb_id": kb_id})
|
||||||
|
emb_engine = (getattr(krecs[0], 'embedding_engine', '') or 'clip-vith14').strip() if krecs else 'clip-vith14'
|
||||||
|
if emb_engine == 'bge-m3':
|
||||||
|
emb_url = 'https://embedding.opencomputing.net:10443/txte/api/embed'
|
||||||
|
emb_model = 'bge-m3'
|
||||||
|
else:
|
||||||
|
emb_url = 'https://embedding.opencomputing.net:10443/mme/api/embed'
|
||||||
|
emb_model = 'CLIP-ViT-H-14'
|
||||||
|
|
||||||
if not rows:
|
if not rows:
|
||||||
return json.dumps({"widgettype": "Text", "options": {"text": "No unprocessed documents found"}}, ensure_ascii=False)
|
return json.dumps({"widgettype": "Text", "options": {"text": "No unprocessed documents found"}}, ensure_ascii=False)
|
||||||
@ -173,8 +181,8 @@ async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=60)) as ses
|
|||||||
|
|
||||||
# Embed
|
# Embed
|
||||||
try:
|
try:
|
||||||
r2 = await session.post('https://embedding.opencomputing.net:10443/api/embed',
|
r2 = await session.post(emb_url,
|
||||||
json={"texts": chunks, "model": "CLIP-ViT-H-14"})
|
json={"texts": chunks, "model": emb_model})
|
||||||
emb = await r2.json() if r2.status == 200 else {}
|
emb = await r2.json() if r2.status == 200 else {}
|
||||||
embeddings = emb.get("text_embeddings", emb.get("embeddings", []))
|
embeddings = emb.get("text_embeddings", emb.get("embeddings", []))
|
||||||
except:
|
except:
|
||||||
|
|||||||
@ -7,10 +7,14 @@ async with db.sqlorContext(dbname) as sor:
|
|||||||
kb_id = uuid()
|
kb_id = uuid()
|
||||||
name = ns.get("name", "")
|
name = ns.get("name", "")
|
||||||
desc = ns.get("description", "")
|
desc = ns.get("description", "")
|
||||||
|
# 向量引擎:clip-vith14=多媒体(CLIP),bge-m3=文本(bge-m3)。默认 clip-vith14 兼容旧数据
|
||||||
|
emb_type = ns.get("embedding_type", "") or ns.get("embedding_engine", "")
|
||||||
|
if emb_type not in ("bge-m3", "clip-vith14"):
|
||||||
|
emb_type = "clip-vith14"
|
||||||
await sor.sqlExe(
|
await sor.sqlExe(
|
||||||
"INSERT INTO knowledge_bases (id, name, description, org_id, embedding_engine, vdb_collection, doc_count, total_size, chunk_count, status, created_at) "
|
"INSERT INTO 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}$, 'CLIP ViT-H-14', 'rag_collection', 0, 0, 0, 'active', NOW())",
|
"VALUES (${id}$, ${name}$, ${desc}$, ${org_id}$, ${emb}$, 'rag_collection', 0, 0, 0, 'active', NOW())",
|
||||||
{"id": kb_id, "name": name, "desc": desc, "org_id": userorgid})
|
{"id": kb_id, "name": name, "desc": desc, "org_id": userorgid, "emb": emb_type})
|
||||||
return {
|
return {
|
||||||
"widgettype": "urlwidget",
|
"widgettype": "urlwidget",
|
||||||
"options": {"url": entire_url('/rag/knowledge_bases_list/index.ui')}
|
"options": {"url": entire_url('/rag/knowledge_bases_list/index.ui')}
|
||||||
|
|||||||
5
wwwroot/knowledge_bases_list/embedding_options.dspy
Normal file
5
wwwroot/knowledge_bases_list/embedding_options.dspy
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import json
|
||||||
|
return json.dumps([
|
||||||
|
{"value": "clip-vith14", "text": "多媒体(图文音视)"},
|
||||||
|
{"value": "bge-m3", "text": "文本(文档检索)"}
|
||||||
|
], ensure_ascii=False)
|
||||||
@ -10,7 +10,8 @@
|
|||||||
"cols": 1,
|
"cols": 1,
|
||||||
"fields": [
|
"fields": [
|
||||||
{"name": "name", "label": "名称", "uitype": "str", "required": true},
|
{"name": "name", "label": "名称", "uitype": "str", "required": true},
|
||||||
{"name": "description", "label": "描述", "uitype": "text"}
|
{"name": "description", "label": "描述", "uitype": "text"},
|
||||||
|
{"name": "embedding_type", "label": "向量引擎", "uitype": "code", "value": "clip-vith14", "dataurl": "{{entire_url('./embedding_options.dspy')}}"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,13 +75,28 @@ hits = []
|
|||||||
raw_rows = []
|
raw_rows = []
|
||||||
kw_rows = []
|
kw_rows = []
|
||||||
|
|
||||||
|
# 读知识库向量引擎,决定用文本(bge-m3 /txte)还是多媒体(CLIP /mme) embedding
|
||||||
|
emb_engine = 'clip-vith14'
|
||||||
|
try:
|
||||||
|
async with get_sor_context(env, 'rag') as sor:
|
||||||
|
krecs = await sor.sqlExe("SELECT embedding_engine FROM knowledge_bases WHERE id=${kb_id}$", {"kb_id": kb_id})
|
||||||
|
if krecs:
|
||||||
|
emb_engine = (getattr(krecs[0], 'embedding_engine', '') or 'clip-vith14').strip()
|
||||||
|
except: pass
|
||||||
|
if emb_engine == 'bge-m3':
|
||||||
|
emb_url = 'https://embedding.opencomputing.net:10443/txte/api/embed'
|
||||||
|
emb_model = 'bge-m3'
|
||||||
|
else:
|
||||||
|
emb_url = 'https://embedding.opencomputing.net:10443/mme/api/embed'
|
||||||
|
emb_model = 'CLIP-ViT-H-14'
|
||||||
|
|
||||||
if query:
|
if query:
|
||||||
# 1. Embed
|
# 1. Embed
|
||||||
vec = []
|
vec = []
|
||||||
try:
|
try:
|
||||||
client = StreamHttpClient()
|
client = StreamHttpClient()
|
||||||
resp = await client.request('POST', 'https://embedding.opencomputing.net:10443/api/embed',
|
resp = await client.request('POST', emb_url,
|
||||||
json={"texts": [query], "model": "CLIP-ViT-H-14"})
|
json={"texts": [query], "model": emb_model})
|
||||||
emb = json.loads(resp)
|
emb = json.loads(resp)
|
||||||
vec = emb.get("text_embeddings", emb.get("embeddings", [[]]))[0]
|
vec = emb.get("text_embeddings", emb.get("embeddings", [[]]))[0]
|
||||||
except:
|
except:
|
||||||
|
|||||||
@ -48,6 +48,22 @@ ext_l = ext.lower()
|
|||||||
async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
|
async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
|
||||||
db = DBPools()
|
db = DBPools()
|
||||||
|
|
||||||
|
# 读知识库向量引擎:bge-m3=文本(走 /txte),clip-vith14=多媒体(走 /mme)
|
||||||
|
emb_engine = 'clip-vith14'
|
||||||
|
try:
|
||||||
|
async with db.sqlorContext('rag') as sor:
|
||||||
|
krecs = await sor.sqlExe("SELECT embedding_engine FROM knowledge_bases WHERE id=${kb_id}$", {"kb_id": kb_id})
|
||||||
|
if krecs:
|
||||||
|
emb_engine = (getattr(krecs[0], 'embedding_engine', '') or 'clip-vith14').strip()
|
||||||
|
except: pass
|
||||||
|
is_text = emb_engine == 'bge-m3'
|
||||||
|
if is_text:
|
||||||
|
emb_url = 'https://embedding.opencomputing.net:10443/txte/api/embed'
|
||||||
|
emb_model = 'bge-m3'
|
||||||
|
else:
|
||||||
|
emb_url = 'https://embedding.opencomputing.net:10443/mme/api/embed'
|
||||||
|
emb_model = 'CLIP-ViT-H-14'
|
||||||
|
|
||||||
async def ensure_vdb_collection(client, kb_id):
|
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"}
|
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)
|
await client.request('POST', 'https://vectordb.opencomputing.net:10443/v1/createcollection', json=payload)
|
||||||
@ -97,6 +113,16 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
|
|||||||
if ext_l in text_exts:
|
if ext_l in text_exts:
|
||||||
text = file_data.decode('utf-8', errors='replace')
|
text = file_data.decode('utf-8', errors='replace')
|
||||||
|
|
||||||
|
# --- 文本知识库不支持媒体文件 ---
|
||||||
|
if is_text and (ext_l in image_exts or ext_l in audio_exts or ext_l in video_exts):
|
||||||
|
try:
|
||||||
|
async with db.sqlorContext('rag') as sor:
|
||||||
|
await sor.sqlExe(
|
||||||
|
"UPDATE documents SET status='failed', metadata=${meta}$, updated_at=NOW() WHERE id=${id}$",
|
||||||
|
{"id": doc_id, "meta": json.dumps({"error": "文本知识库不支持媒体文件,请上传文本类文件(txt/md/pdf/docx等)或改用多媒体知识库"}, ensure_ascii=False)})
|
||||||
|
except: pass
|
||||||
|
return
|
||||||
|
|
||||||
# --- IMAGE: face detection ---
|
# --- IMAGE: face detection ---
|
||||||
if ext_l in image_exts:
|
if ext_l in image_exts:
|
||||||
img_b64 = base64.b64encode(file_data).decode()
|
img_b64 = base64.b64encode(file_data).decode()
|
||||||
@ -150,8 +176,8 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
|
|||||||
# --- CLIP image embedding for video frame ---
|
# --- CLIP image embedding for video frame ---
|
||||||
try:
|
try:
|
||||||
client2 = StreamHttpClient()
|
client2 = StreamHttpClient()
|
||||||
resp2 = await client2.request('POST', 'https://embedding.opencomputing.net:10443/api/embed',
|
resp2 = await client2.request('POST', emb_url,
|
||||||
json={"images": [img_b64], "model": "CLIP-ViT-H-14"})
|
json={"images": [img_b64], "model": emb_model})
|
||||||
emb_data = json.loads(resp2)
|
emb_data = json.loads(resp2)
|
||||||
img_embeddings = emb_data.get("image_embeddings", emb_data.get("embeddings", []))
|
img_embeddings = emb_data.get("image_embeddings", emb_data.get("embeddings", []))
|
||||||
except:
|
except:
|
||||||
@ -226,8 +252,8 @@ async def ingest_doc(doc_id, kb_id, file_name, ext_l, real_path):
|
|||||||
if chunks:
|
if chunks:
|
||||||
try:
|
try:
|
||||||
client = StreamHttpClient()
|
client = StreamHttpClient()
|
||||||
resp = await client.request('POST', 'https://embedding.opencomputing.net:10443/api/embed',
|
resp = await client.request('POST', emb_url,
|
||||||
json={"texts": chunks, "model": "CLIP-ViT-H-14"})
|
json={"texts": chunks, "model": emb_model})
|
||||||
emb_data = json.loads(resp)
|
emb_data = json.loads(resp)
|
||||||
embeddings = emb_data.get("text_embeddings", emb_data.get("embeddings", []))
|
embeddings = emb_data.get("text_embeddings", emb_data.get("embeddings", []))
|
||||||
except:
|
except:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user