mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-31 04:45:23 +08:00
feat(plugins): consolidate official plugins with independent releases (LANG-180) (#2797)
Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
co-authored by
multica-agent
parent
d099033d79
commit
3a77306c3c
@@ -0,0 +1,545 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from components.shared_state import (
|
||||
AmbiguousMutationError,
|
||||
ConfigStore,
|
||||
FenceLifecycleError,
|
||||
serialized,
|
||||
)
|
||||
|
||||
import httpx
|
||||
|
||||
from langbot_plugin.api.definition.components.knowledge_engine import KnowledgeEngine, KnowledgeEngineCapability
|
||||
from langbot_plugin.api.entities.builtin.rag import (
|
||||
IngestionContext,
|
||||
IngestionResult,
|
||||
DocumentStatus,
|
||||
RetrievalContext,
|
||||
RetrievalResultEntry,
|
||||
RetrievalResponse,
|
||||
)
|
||||
from langbot_plugin.api.entities.builtin.provider.message import ContentElement
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _normalize_base_url(api_base_url: str) -> str:
|
||||
"""Reduce a configured base URL to ``scheme://host[:port]/path``.
|
||||
|
||||
A document mapping must name the upstream it was created on, so the stored
|
||||
form is canonical: scheme and host casing, a default port and a trailing
|
||||
slash never distinguish one deployment from another. An address without a
|
||||
host is stored stripped, so a later comparison still detects a change.
|
||||
"""
|
||||
url = httpx.URL(api_base_url)
|
||||
if not url.host:
|
||||
return api_base_url.rstrip("/")
|
||||
default_port = {'http': 80, 'https': 443}.get(url.scheme)
|
||||
port = f":{url.port}" if url.port is not None and url.port != default_port else ""
|
||||
return f"{url.scheme}://{url.host}{port}{url.path.rstrip('/')}"
|
||||
|
||||
|
||||
class RAGFlowConnector(ConfigStore, KnowledgeEngine):
|
||||
"""Knowledge Engine powered by RAGFlow.
|
||||
|
||||
Supports retrieval, document ingestion (upload + parse), and deletion
|
||||
via the RAGFlow HTTP API.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_capabilities(cls) -> list[str]:
|
||||
return [KnowledgeEngineCapability.DOC_INGESTION, KnowledgeEngineCapability.DOC_PARSING]
|
||||
|
||||
# ========== Lifecycle Hooks ==========
|
||||
|
||||
@serialized
|
||||
async def on_knowledge_base_create(self, kb_id: str, config: dict) -> None:
|
||||
"""Persist installation-bound configuration and validate dataset IDs."""
|
||||
logger.info(f"[RAGFlowKnowledgeEngine] Knowledge base created: {kb_id}")
|
||||
await self._save_config(kb_id, config)
|
||||
|
||||
# Validate dataset IDs
|
||||
api_base_url = config.get("api_base_url", "http://localhost:9380").rstrip("/")
|
||||
api_key = config.get("api_key")
|
||||
dataset_ids_str = config.get("dataset_ids", "")
|
||||
|
||||
if not api_key or not dataset_ids_str:
|
||||
return
|
||||
|
||||
dataset_ids = [did.strip() for did in dataset_ids_str.split(",") if did.strip()]
|
||||
if not dataset_ids:
|
||||
return
|
||||
|
||||
try:
|
||||
async with self.http_client() as client:
|
||||
resp = await client.get(
|
||||
f"{api_base_url}/api/v1/datasets",
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
params={"page": 1, "page_size": 100},
|
||||
timeout=15.0,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
|
||||
if data.get("code") != 0:
|
||||
logger.warning(
|
||||
f"[RAGFlowKnowledgeEngine] Failed to validate datasets: {data.get('message')}"
|
||||
)
|
||||
return
|
||||
|
||||
existing_ids = {ds["id"] for ds in data.get("data", [])}
|
||||
for did in dataset_ids:
|
||||
if did in existing_ids:
|
||||
logger.info(f"[RAGFlowKnowledgeEngine] Dataset {did} validated OK")
|
||||
else:
|
||||
logger.warning(
|
||||
f"[RAGFlowKnowledgeEngine] Dataset {did} NOT found in RAGFlow! "
|
||||
"Please check the dataset ID."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"[RAGFlowKnowledgeEngine] Dataset validation failed: {e}")
|
||||
|
||||
@serialized(allow_fenced=True)
|
||||
async def on_knowledge_base_delete(self, kb_id: str) -> None:
|
||||
"""Tombstone the stored configuration and clear any fence for this KB."""
|
||||
logger.info(f"[RAGFlowKnowledgeEngine] Knowledge base deleted: {kb_id}")
|
||||
await self._save_config(kb_id, None)
|
||||
# Deleting the knowledge base is the operator path out of a fence: the
|
||||
# pending ambiguity is moot once it no longer exists.
|
||||
await self._state.clear_fence(self.plugin, kb_id)
|
||||
|
||||
async def retrieve(self, context: RetrievalContext) -> RetrievalResponse:
|
||||
"""Execute retrieval against RAGFlow API."""
|
||||
config = context.creation_settings
|
||||
retrieval = context.retrieval_settings
|
||||
|
||||
api_base_url = config.get("api_base_url", "http://localhost:9380").rstrip("/")
|
||||
api_key = config.get("api_key")
|
||||
dataset_ids_str = config.get("dataset_ids", "")
|
||||
top_k = retrieval.get("top_k", 1024)
|
||||
similarity_threshold = retrieval.get("similarity_threshold", 0.2)
|
||||
vector_similarity_weight = retrieval.get("vector_similarity_weight", 0.3)
|
||||
page_size = retrieval.get("page_size", 30)
|
||||
keyword = retrieval.get("keyword", False)
|
||||
rerank_id = retrieval.get("rerank_id", "")
|
||||
use_kg = retrieval.get("use_kg", False)
|
||||
|
||||
if not api_key or not dataset_ids_str:
|
||||
logger.error(
|
||||
f"[RAGFlowKnowledgeEngine] Missing required configuration. "
|
||||
f"Config keys: {list(config.keys())}"
|
||||
)
|
||||
return RetrievalResponse(results=[], total_found=0)
|
||||
|
||||
dataset_ids = [did.strip() for did in dataset_ids_str.split(",") if did.strip()]
|
||||
|
||||
if not dataset_ids:
|
||||
logger.error("[RAGFlowKnowledgeEngine] No valid dataset IDs provided")
|
||||
return RetrievalResponse(results=[], total_found=0)
|
||||
|
||||
url = f"{api_base_url}/api/v1/retrieval"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
payload = {
|
||||
"question": context.query,
|
||||
"dataset_ids": dataset_ids,
|
||||
"page": 1,
|
||||
"page_size": int(page_size),
|
||||
"similarity_threshold": float(similarity_threshold),
|
||||
"vector_similarity_weight": float(vector_similarity_weight),
|
||||
"top_k": int(top_k),
|
||||
"keyword": keyword,
|
||||
"highlight": False,
|
||||
"use_kg": use_kg,
|
||||
}
|
||||
if rerank_id:
|
||||
payload["rerank_id"] = rerank_id
|
||||
|
||||
results: list[RetrievalResultEntry] = []
|
||||
try:
|
||||
async with self.http_client() as client:
|
||||
response = await client.post(url, json=payload, headers=headers, timeout=30.0)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
if data.get("code") != 0:
|
||||
logger.error(
|
||||
f"[RAGFlowKnowledgeEngine] API returned error code: {data.get('code')}"
|
||||
)
|
||||
return RetrievalResponse(results=[], total_found=0)
|
||||
|
||||
logger.info(
|
||||
f"[RAGFlowKnowledgeEngine] Request: vector_weight={vector_similarity_weight}, "
|
||||
f"top_k={top_k}, page_size={page_size}, threshold={similarity_threshold}, "
|
||||
f"keyword={keyword}, rerank_id={rerank_id or 'None'}, use_kg={use_kg}, "
|
||||
f"Response chunks: {len(data.get('data', {}).get('chunks', []))}"
|
||||
)
|
||||
|
||||
for chunk in data.get("data", {}).get("chunks", []):
|
||||
similarity = chunk.get("similarity")
|
||||
if similarity is None:
|
||||
similarity = 0.0
|
||||
|
||||
results.append(
|
||||
RetrievalResultEntry(
|
||||
id=chunk.get("id", ""),
|
||||
content=[ContentElement.from_text(chunk.get("content", ""))],
|
||||
metadata={
|
||||
"document_id": chunk.get("document_id", ""),
|
||||
"kb_id": chunk.get("kb_id", ""),
|
||||
"document_keyword": chunk.get("document_keyword", ""),
|
||||
"important_keywords": chunk.get("important_keywords", []),
|
||||
"term_similarity": chunk.get("term_similarity", 0.0),
|
||||
"vector_similarity": chunk.get("vector_similarity", 0.0),
|
||||
"image_id": chunk.get("image_id"),
|
||||
},
|
||||
distance=1.0 - float(similarity),
|
||||
score=float(similarity),
|
||||
)
|
||||
)
|
||||
|
||||
logger.info(f"[RAGFlowKnowledgeEngine] Retrieved {len(results)} chunks from RAGFlow.")
|
||||
except Exception:
|
||||
logger.exception("[RAGFlowKnowledgeEngine] Error during retrieval")
|
||||
|
||||
return RetrievalResponse(results=results, total_found=len(results))
|
||||
|
||||
@serialized
|
||||
async def ingest(self, context: IngestionContext) -> IngestionResult:
|
||||
"""Upload a file to RAGFlow and trigger parsing."""
|
||||
doc_id = context.file_object.metadata.document_id
|
||||
filename = context.file_object.metadata.filename
|
||||
|
||||
config = context.creation_settings
|
||||
api_base_url = config.get("api_base_url", "http://localhost:9380").rstrip("/")
|
||||
api_key = config.get("api_key")
|
||||
dataset_ids_str = config.get("dataset_ids", "")
|
||||
|
||||
if not api_key or not dataset_ids_str:
|
||||
logger.error(
|
||||
f"[RAGFlowKnowledgeEngine] Missing required configuration for ingestion. "
|
||||
f"Config keys: {list(config.keys())}"
|
||||
)
|
||||
return IngestionResult(
|
||||
document_id=doc_id,
|
||||
status=DocumentStatus.FAILED,
|
||||
error_message="Missing api_key or dataset_ids in configuration.",
|
||||
)
|
||||
|
||||
dataset_ids = [did.strip() for did in dataset_ids_str.split(",") if did.strip()]
|
||||
if not dataset_ids:
|
||||
logger.error("[RAGFlowKnowledgeEngine] No valid dataset IDs provided for ingestion")
|
||||
return IngestionResult(
|
||||
document_id=doc_id,
|
||||
status=DocumentStatus.FAILED,
|
||||
error_message="No valid dataset IDs provided.",
|
||||
)
|
||||
|
||||
# Use the first dataset as the ingestion target
|
||||
target_dataset_id = dataset_ids[0]
|
||||
# The mapping names the canonical upstream it was created on, so a later
|
||||
# configuration change cannot be mistaken for the same deployment.
|
||||
upstream_target = _normalize_base_url(api_base_url)
|
||||
|
||||
# The Host knowledge-base id is the identity every entry point shares — the
|
||||
# create hook, deletion and the fence/lock scope — so the collection id,
|
||||
# which is a vector-store identity that need not equal it, is not used.
|
||||
kb_id = context.knowledge_base_id
|
||||
await self._save_config(kb_id, config)
|
||||
if await self._load_document(kb_id, doc_id) is not None:
|
||||
raise RuntimeError('Existing upload intent; reconcile before retry')
|
||||
|
||||
# 1. Read file content from Host
|
||||
try:
|
||||
file_bytes = await self.plugin.get_knowledge_file_stream(context.file_object.storage_path)
|
||||
except Exception as e:
|
||||
logger.error(f"[RAGFlowKnowledgeEngine] Failed to get file content: {e}")
|
||||
return IngestionResult(
|
||||
document_id=doc_id,
|
||||
status=DocumentStatus.FAILED,
|
||||
error_message=f"Could not read file: {e}",
|
||||
)
|
||||
|
||||
await self._save_document(kb_id, doc_id, {'upstream_id': '',
|
||||
'dataset_id': target_dataset_id, 'api_base_url': upstream_target,
|
||||
'status': 'pending'})
|
||||
headers = {"Authorization": f"Bearer {api_key}"}
|
||||
|
||||
try:
|
||||
async with self.http_client() as client:
|
||||
# 2. Upload file to RAGFlow dataset
|
||||
upload_url = f"{api_base_url}/api/v1/datasets/{target_dataset_id}/documents"
|
||||
files = {"file": (filename, file_bytes)}
|
||||
|
||||
async def upload():
|
||||
response = await client.post(
|
||||
upload_url, headers=headers, files=files, timeout=60.0
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if data.get("code") != 0:
|
||||
# The provider answered: the upload was rejected, so no
|
||||
# Host record is written and the fence can be released.
|
||||
return None, data.get("message") or "Unknown upload error"
|
||||
# Extract the document ID returned by RAGFlow
|
||||
docs = data.get("data", [])
|
||||
ragflow_doc_id = docs[0].get("id") if docs and isinstance(docs[0], dict) else None
|
||||
if not isinstance(ragflow_doc_id, str) or not ragflow_doc_id:
|
||||
raise AmbiguousMutationError("RAGFlow upload omitted upstream document ID; outcome requires reconciliation")
|
||||
# The mapping is recorded before the fence is released: a
|
||||
# cancellation or a Host failure here must not leave an
|
||||
# uploaded document with no durable record of it.
|
||||
await self._save_document(kb_id, doc_id, {'upstream_id': ragflow_doc_id,
|
||||
'dataset_id': target_dataset_id, 'api_base_url': upstream_target,
|
||||
'status': 'created'})
|
||||
return ragflow_doc_id, None
|
||||
|
||||
# The intent is persisted before the upload leaves, and the fence
|
||||
# is released only once the response was validated and the mapping
|
||||
# recorded.
|
||||
ragflow_doc_id, upload_error = await self._state.dispatched(
|
||||
self.plugin, kb_id, upload, 'RAGFlow upload outcome unknown',
|
||||
)
|
||||
if upload_error is not None:
|
||||
logger.error(f"[RAGFlowKnowledgeEngine] Upload failed: {upload_error}")
|
||||
return IngestionResult(
|
||||
document_id=doc_id,
|
||||
status=DocumentStatus.FAILED,
|
||||
error_message=f"RAGFlow upload error: {upload_error}",
|
||||
)
|
||||
|
||||
# 3. Trigger parsing
|
||||
chunks_url = f"{api_base_url}/api/v1/datasets/{target_dataset_id}/chunks"
|
||||
|
||||
async def trigger_parse():
|
||||
response = await client.post(
|
||||
chunks_url,
|
||||
headers={**headers, "Content-Type": "application/json"},
|
||||
json={"document_ids": [ragflow_doc_id]},
|
||||
timeout=30.0,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if data.get("code") != 0:
|
||||
# The provider answered: parsing did not start, so the
|
||||
# fence can be released.
|
||||
return data.get("message") or "Unknown parsing error"
|
||||
return None
|
||||
|
||||
parse_error = await self._state.dispatched(
|
||||
self.plugin, kb_id,
|
||||
trigger_parse, 'RAGFlow parsing trigger outcome unknown',
|
||||
)
|
||||
if parse_error is not None:
|
||||
logger.warning(
|
||||
f"[RAGFlowKnowledgeEngine] Parsing trigger returned error: {parse_error}"
|
||||
)
|
||||
# Document was uploaded but parsing failed to start
|
||||
return IngestionResult(
|
||||
document_id=ragflow_doc_id,
|
||||
status=DocumentStatus.FAILED,
|
||||
error_message=f"RAGFlow parsing trigger error: {parse_error}",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"[RAGFlowKnowledgeEngine] File '{filename}' uploaded and parsing triggered "
|
||||
f"(ragflow_doc_id={ragflow_doc_id})"
|
||||
)
|
||||
|
||||
# Auto-trigger GraphRAG construction if enabled
|
||||
auto_graphrag = config.get("auto_graphrag", False)
|
||||
if auto_graphrag:
|
||||
graphrag_url = f"{api_base_url}/api/v1/datasets/{target_dataset_id}/run_graphrag"
|
||||
|
||||
async def trigger_graphrag():
|
||||
response = await client.post(
|
||||
graphrag_url,
|
||||
headers={**headers, "Content-Type": "application/json"},
|
||||
timeout=30.0,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if data.get("code") != 0:
|
||||
# The provider answered: GraphRAG did not start. That
|
||||
# is a resolved answer, not an unresolved state.
|
||||
return None, data.get("message") or "unknown reason"
|
||||
task = (data.get("data") or {}).get("graphrag_task_id", "unknown")
|
||||
return task, None
|
||||
|
||||
# An unresolved GraphRAG outcome is not swallowed: it keeps
|
||||
# its own fence and the chain must not continue over it (the
|
||||
# RAPTOR dispatch would refuse it, and a later success must
|
||||
# never clear this fence).
|
||||
graphrag_task_id, graphrag_error = await self._state.dispatched(
|
||||
self.plugin, kb_id, trigger_graphrag,
|
||||
'RAGFlow GraphRAG trigger outcome unknown',
|
||||
)
|
||||
if graphrag_error is None:
|
||||
logger.info(
|
||||
f"[RAGFlowKnowledgeEngine] GraphRAG construction triggered "
|
||||
f"(task_id={graphrag_task_id})"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"[RAGFlowKnowledgeEngine] GraphRAG trigger returned: {graphrag_error}"
|
||||
)
|
||||
|
||||
# Auto-trigger RAPTOR construction if enabled
|
||||
auto_raptor = config.get("auto_raptor", False)
|
||||
if auto_raptor:
|
||||
raptor_url = f"{api_base_url}/api/v1/datasets/{target_dataset_id}/run_raptor"
|
||||
|
||||
async def trigger_raptor():
|
||||
response = await client.post(
|
||||
raptor_url,
|
||||
headers={**headers, "Content-Type": "application/json"},
|
||||
timeout=30.0,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if data.get("code") != 0:
|
||||
# The provider answered: RAPTOR did not start.
|
||||
return None, data.get("message") or "unknown reason"
|
||||
task = (data.get("data") or {}).get("raptor_task_id", "unknown")
|
||||
return task, None
|
||||
|
||||
raptor_task_id, raptor_error = await self._state.dispatched(
|
||||
self.plugin, kb_id, trigger_raptor,
|
||||
'RAGFlow RAPTOR trigger outcome unknown',
|
||||
)
|
||||
if raptor_error is None:
|
||||
logger.info(
|
||||
f"[RAGFlowKnowledgeEngine] RAPTOR construction triggered "
|
||||
f"(task_id={raptor_task_id})"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"[RAGFlowKnowledgeEngine] RAPTOR trigger returned: {raptor_error}"
|
||||
)
|
||||
|
||||
return IngestionResult(
|
||||
document_id=ragflow_doc_id,
|
||||
status=DocumentStatus.PROCESSING,
|
||||
)
|
||||
|
||||
except (AmbiguousMutationError, FenceLifecycleError):
|
||||
# An already-dispatched mutation with an unknown outcome, or a
|
||||
# mutation that was never dispatched: the caller must see it.
|
||||
raise
|
||||
except Exception as e:
|
||||
# An ambiguous failure already left its pre-dispatch fence in place.
|
||||
logger.error(f"[RAGFlowKnowledgeEngine] Ingestion failed for {filename}: {e}")
|
||||
return IngestionResult(
|
||||
document_id=doc_id,
|
||||
status=DocumentStatus.FAILED,
|
||||
error_message=str(e),
|
||||
)
|
||||
|
||||
@serialized
|
||||
async def delete_document(self, kb_id: str, document_id: str) -> bool:
|
||||
"""Delete a document from RAGFlow."""
|
||||
mapping = await self._load_document(kb_id, document_id)
|
||||
if not mapping or not mapping['upstream_id'] or mapping['status'] != 'created':
|
||||
return False
|
||||
config = await self._load_config(kb_id)
|
||||
if not config:
|
||||
logger.error(
|
||||
f"[RAGFlowKnowledgeEngine] No stored config for kb_id={kb_id}, "
|
||||
"cannot delete document"
|
||||
)
|
||||
return False
|
||||
|
||||
api_base_url = config.get("api_base_url", "http://localhost:9380").rstrip("/")
|
||||
api_key = config.get("api_key")
|
||||
dataset_ids_str = config.get("dataset_ids", "")
|
||||
|
||||
if not api_key or not dataset_ids_str:
|
||||
logger.error(
|
||||
f"[RAGFlowKnowledgeEngine] Missing api_key or dataset_ids for kb_id={kb_id}"
|
||||
)
|
||||
return False
|
||||
|
||||
dataset_ids = [did.strip() for did in dataset_ids_str.split(",") if did.strip()]
|
||||
if not dataset_ids:
|
||||
logger.error(
|
||||
f"[RAGFlowKnowledgeEngine] No valid dataset IDs for kb_id={kb_id}"
|
||||
)
|
||||
return False
|
||||
|
||||
target_dataset_id = dataset_ids[0]
|
||||
|
||||
if mapping['dataset_id'] != target_dataset_id:
|
||||
# Local validation before any remote call: deterministic, no fence.
|
||||
raise RuntimeError('Dataset changed; reconcile before deletion')
|
||||
if mapping.get('api_base_url') != _normalize_base_url(api_base_url):
|
||||
# The recorded upstream is not the configured one, so the document id
|
||||
# does not necessarily exist here; mappings written before the target
|
||||
# was recorded are equally unresolvable.
|
||||
raise RuntimeError('Upstream target changed; reconcile before deletion')
|
||||
|
||||
url = f"{api_base_url}/api/v1/datasets/{target_dataset_id}/documents"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
async def delete_upstream():
|
||||
async with self.http_client() as client:
|
||||
response = await client.request(
|
||||
"DELETE", url, headers=headers,
|
||||
json={"ids": [mapping['upstream_id']]},
|
||||
timeout=30.0,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
if data.get("code") != 0:
|
||||
# The provider answered: the delete was rejected, so there is
|
||||
# nothing to record and the fence can be released.
|
||||
return False, data.get("message") or "Unknown error"
|
||||
# The tombstone is recorded before the fence is released: a
|
||||
# cancellation or a Host failure here must not leave a deleted
|
||||
# document that the mapping still calls created.
|
||||
await self._save_document(kb_id, mapping['host_document_id'], {**mapping, 'status': 'deleted'})
|
||||
return True, None
|
||||
|
||||
try:
|
||||
deleted, error = await self._state.dispatched(
|
||||
self.plugin, kb_id, delete_upstream, 'RAGFlow delete outcome unknown',
|
||||
)
|
||||
|
||||
if not deleted:
|
||||
logger.error(
|
||||
f"[RAGFlowKnowledgeEngine] Delete failed for doc={document_id}: {error}"
|
||||
)
|
||||
return False
|
||||
|
||||
logger.info(
|
||||
f"[RAGFlowKnowledgeEngine] Document {document_id} deleted from "
|
||||
f"dataset {target_dataset_id}"
|
||||
)
|
||||
return True
|
||||
|
||||
except (AmbiguousMutationError, FenceLifecycleError):
|
||||
# The delete was dispatched with an unknown outcome, or never
|
||||
# dispatched at all: the caller must not read this as a plain failure.
|
||||
raise
|
||||
except httpx.HTTPStatusError as e:
|
||||
# The provider answered, so this is not a transport ambiguity: a 4xx
|
||||
# rejection is a resolved failure whose fence was released, while a
|
||||
# 5xx keeps its fence (the provider or an intermediary may have
|
||||
# committed the delete and then failed), so the next mutation for
|
||||
# this knowledge base is still refused.
|
||||
logger.error(
|
||||
f"[RAGFlowKnowledgeEngine] Delete rejected for doc={document_id}: {e}"
|
||||
)
|
||||
return False
|
||||
except Exception:
|
||||
# An ambiguous failure already left its pre-dispatch fence in place.
|
||||
logger.exception(
|
||||
f"[RAGFlowKnowledgeEngine] Error deleting document {document_id}"
|
||||
)
|
||||
return False
|
||||
@@ -0,0 +1,161 @@
|
||||
apiVersion: v1
|
||||
kind: KnowledgeEngine
|
||||
metadata:
|
||||
name: ragflow-connector
|
||||
label:
|
||||
en_US: RAGFlow Knowledge Base
|
||||
zh_Hans: RAGFlow 知识库
|
||||
ja_JP: RAGFlow 知識ベース
|
||||
description:
|
||||
en_US: Retrieve knowledge from or store files into RAGFlow knowledge bases
|
||||
zh_Hans: 从 RAGFlow 知识库中检索知识或向其中存入文件
|
||||
ja_JP: RAGFlow 知識ベースから知識を検索またはファイルを保存
|
||||
spec:
|
||||
creation_schema:
|
||||
- name: api_base_url
|
||||
label:
|
||||
en_US: API Base URL
|
||||
zh_Hans: API 基础地址
|
||||
ja_JP: API ベース URL
|
||||
description:
|
||||
en_US: 'Base URL for RAGFlow API (e.g., http://localhost:9380 for local, or your server URL)'
|
||||
zh_Hans: 'RAGFlow API 基础地址(本地版本使用 http://localhost:9380,或使用您的服务器地址)'
|
||||
ja_JP: 'RAGFlow API ベース URL(ローカル版は http://localhost:9380、またはサーバーの URL)'
|
||||
type: string
|
||||
default: http://localhost:9380
|
||||
required: true
|
||||
- name: api_key
|
||||
label:
|
||||
en_US: API Key
|
||||
zh_Hans: API 密钥
|
||||
ja_JP: API キー
|
||||
description:
|
||||
en_US: 'API key from your RAGFlow instance'
|
||||
zh_Hans: '从您的 RAGFlow 实例获取的 API Key'
|
||||
ja_JP: 'RAGFlow インスタンスから取得した API Key'
|
||||
type: password
|
||||
required: true
|
||||
- name: dataset_ids
|
||||
label:
|
||||
en_US: Dataset IDs
|
||||
zh_Hans: 数据集 IDs
|
||||
ja_JP: データセット ID
|
||||
description:
|
||||
en_US: 'Comma-separated dataset IDs to search (e.g., "id1,id2,id3")'
|
||||
zh_Hans: '要搜索的数据集 ID,用逗号分隔(例如:"id1,id2,id3")'
|
||||
ja_JP: '検索するデータセット ID(カンマ区切り、例:"id1,id2,id3")'
|
||||
type: string
|
||||
required: true
|
||||
- name: auto_graphrag
|
||||
label:
|
||||
en_US: Auto Build Knowledge Graph
|
||||
zh_Hans: 自动构建知识图谱
|
||||
ja_JP: ナレッジグラフ自動構築
|
||||
description:
|
||||
en_US: 'Automatically trigger GraphRAG construction after file ingestion'
|
||||
zh_Hans: '文件上传解析后自动触发知识图谱构建'
|
||||
ja_JP: 'ファイル取り込み後に自動的にナレッジグラフを構築'
|
||||
type: boolean
|
||||
default: false
|
||||
required: false
|
||||
- name: auto_raptor
|
||||
label:
|
||||
en_US: Auto Build RAPTOR
|
||||
zh_Hans: 自动构建 RAPTOR
|
||||
ja_JP: RAPTOR 自動構築
|
||||
description:
|
||||
en_US: 'Automatically trigger RAPTOR hierarchical summarization after file ingestion'
|
||||
zh_Hans: '文件上传解析后自动触发 RAPTOR 层次摘要构建'
|
||||
ja_JP: 'ファイル取り込み後に自動的に RAPTOR 階層要約を構築'
|
||||
type: boolean
|
||||
default: false
|
||||
required: false
|
||||
retrieval_schema:
|
||||
- name: vector_similarity_weight
|
||||
label:
|
||||
en_US: Vector Similarity Weight
|
||||
zh_Hans: 向量相似度权重
|
||||
ja_JP: ベクトル類似性の重み
|
||||
description:
|
||||
en_US: 'Balance between keyword and vector search (0=keyword only, 1=vector only, default 0.3)'
|
||||
zh_Hans: '关键词与向量检索的平衡(0=纯关键词,1=纯向量,默认 0.3)'
|
||||
ja_JP: 'キーワードとベクトル検索のバランス(0=キーワードのみ、1=ベクトルのみ、デフォルト 0.3)'
|
||||
type: float
|
||||
default: 0.3
|
||||
required: false
|
||||
- name: top_k
|
||||
label:
|
||||
en_US: Top K
|
||||
zh_Hans: 候选数量
|
||||
ja_JP: 候補数
|
||||
description:
|
||||
en_US: 'Maximum number of candidates to consider before pagination'
|
||||
zh_Hans: '分页前考虑的最大候选数量'
|
||||
ja_JP: 'ページネーション前に考慮する最大候補数'
|
||||
type: integer
|
||||
default: 1024
|
||||
required: false
|
||||
- name: page_size
|
||||
label:
|
||||
en_US: Page Size
|
||||
zh_Hans: 返回数量
|
||||
ja_JP: ページサイズ
|
||||
description:
|
||||
en_US: 'Number of results to return'
|
||||
zh_Hans: '返回的结果数量'
|
||||
ja_JP: '返す結果の数'
|
||||
type: integer
|
||||
default: 30
|
||||
required: false
|
||||
- name: similarity_threshold
|
||||
label:
|
||||
en_US: Similarity Threshold
|
||||
zh_Hans: 相似度阈值
|
||||
ja_JP: 類似度しきい値
|
||||
description:
|
||||
en_US: 'Minimum similarity score (0-1)'
|
||||
zh_Hans: '最小相似度分数 (0-1)'
|
||||
ja_JP: '最小類似性スコア (0-1)'
|
||||
type: float
|
||||
default: 0.2
|
||||
required: false
|
||||
- name: keyword
|
||||
label:
|
||||
en_US: LLM Keyword Extraction
|
||||
zh_Hans: LLM 关键词提取
|
||||
ja_JP: LLM キーワード抽出
|
||||
description:
|
||||
en_US: 'Use LLM to extract keywords from query to enhance retrieval (increases response time)'
|
||||
zh_Hans: '使用 LLM 从查询中提取关键词以增强检索(会增加响应时间)'
|
||||
ja_JP: 'LLM でクエリからキーワードを抽出して検索を強化(応答時間が増加)'
|
||||
type: boolean
|
||||
default: false
|
||||
required: false
|
||||
- name: rerank_id
|
||||
label:
|
||||
en_US: Rerank Model ID
|
||||
zh_Hans: 重排序模型 ID
|
||||
ja_JP: リランクモデル ID
|
||||
description:
|
||||
en_US: 'Rerank model ID configured in RAGFlow (e.g., BAAI/bge-reranker-v2-m3). Leave empty to disable'
|
||||
zh_Hans: '在 RAGFlow 中配置的重排序模型 ID(如 BAAI/bge-reranker-v2-m3),留空则不启用'
|
||||
ja_JP: 'RAGFlow で設定されたリランクモデル ID(例:BAAI/bge-reranker-v2-m3)、空欄で無効'
|
||||
type: string
|
||||
default: ''
|
||||
required: false
|
||||
- name: use_kg
|
||||
label:
|
||||
en_US: Knowledge Graph
|
||||
zh_Hans: 知识图谱
|
||||
ja_JP: ナレッジグラフ
|
||||
description:
|
||||
en_US: 'Enable knowledge graph retrieval (increases response time)'
|
||||
zh_Hans: '启用知识图谱检索(会增加响应时间)'
|
||||
ja_JP: 'ナレッジグラフ検索を有効化(応答時間が増加)'
|
||||
type: boolean
|
||||
default: false
|
||||
required: false
|
||||
execution:
|
||||
python:
|
||||
path: engine.py
|
||||
attr: RAGFlowConnector
|
||||
Reference in New Issue
Block a user