feat: add governed knowledge lifecycle
This commit is contained in:
@@ -1,8 +1,43 @@
|
||||
from ninja import Router
|
||||
from django.db import transaction
|
||||
from django.http import HttpResponse
|
||||
from django.utils.http import content_disposition_header
|
||||
from ninja import File, Router
|
||||
from ninja.errors import HttpError
|
||||
from ninja.files import UploadedFile
|
||||
|
||||
from apps.accounts.api import require_permission
|
||||
from apps.knowledge.models import KnowledgeBase, KnowledgeItem
|
||||
from apps.knowledge.documents import create_uploaded_document, prepare_document
|
||||
from apps.knowledge.embeddings import KnowledgeEmbeddingError
|
||||
from apps.knowledge.governance import (
|
||||
AssetGovernanceError,
|
||||
approve_asset_version,
|
||||
compare_asset_versions,
|
||||
create_asset_revision,
|
||||
ensure_governed_asset,
|
||||
offline_asset,
|
||||
reject_asset_version,
|
||||
rollback_asset,
|
||||
submit_asset_version,
|
||||
update_asset_version,
|
||||
)
|
||||
from apps.knowledge.models import (
|
||||
GovernedAsset,
|
||||
GovernedAssetType,
|
||||
GovernedAssetVersion,
|
||||
KnowledgeBase,
|
||||
KnowledgeDocument,
|
||||
KnowledgeItem,
|
||||
TroubleshootingPlan,
|
||||
)
|
||||
from apps.knowledge.schemas import (
|
||||
AssetImpactOut,
|
||||
AssetReviewIn,
|
||||
AssetRevisionIn,
|
||||
AssetRollbackIn,
|
||||
AssetVersionComparisonOut,
|
||||
AssetVersionUpdateIn,
|
||||
GovernedAssetOut,
|
||||
GovernedAssetVersionOut,
|
||||
KnowledgeBaseIn,
|
||||
KnowledgeBaseOut,
|
||||
KnowledgeDocumentIn,
|
||||
@@ -11,12 +46,64 @@ from apps.knowledge.schemas import (
|
||||
KnowledgeItemOut,
|
||||
KnowledgeSearchIn,
|
||||
KnowledgeSearchResultOut,
|
||||
TroubleshootingPlanIn,
|
||||
TroubleshootingPlanOut,
|
||||
)
|
||||
from apps.knowledge.services import ingest_document_text, rebuild_item_chunks, retrieve_knowledge
|
||||
from apps.knowledge.services import ingest_document_text, retrieve_knowledge
|
||||
from apps.knowledge.tasks import dispatch_knowledge_document_processing
|
||||
from apps.solutions.models import TicketSolutionLink
|
||||
from apps.tickets.materials import MaterialValidationError
|
||||
from apps.tickets.models import TicketCategory, TicketSupportAnalysis
|
||||
from apps.tickets.storage import get_object
|
||||
|
||||
router = Router(tags=["knowledge"])
|
||||
|
||||
|
||||
def serialize_asset_version(version: GovernedAssetVersion) -> dict:
|
||||
return {
|
||||
"id": version.id,
|
||||
"version": version.version,
|
||||
"status": version.status,
|
||||
"content": version.content,
|
||||
"change_summary": version.change_summary,
|
||||
"source_version": version.source_version.version
|
||||
if version.source_version_id
|
||||
else None,
|
||||
"created_by_id": version.created_by_id,
|
||||
"submitted_by_id": version.submitted_by_id,
|
||||
"reviewed_by_id": version.reviewed_by_id,
|
||||
"review_comment": version.review_comment,
|
||||
"created_at": version.created_at,
|
||||
"updated_at": version.updated_at,
|
||||
"submitted_at": version.submitted_at,
|
||||
"reviewed_at": version.reviewed_at,
|
||||
"published_at": version.published_at,
|
||||
"offline_at": version.offline_at,
|
||||
}
|
||||
|
||||
|
||||
def serialize_governed_asset(asset: GovernedAsset) -> dict:
|
||||
versions = list(asset.versions.select_related("source_version").all())
|
||||
return {
|
||||
"public_id": asset.public_id,
|
||||
"asset_type": asset.asset_type,
|
||||
"object_id": asset.object_id,
|
||||
"current_published_version": (
|
||||
asset.current_published_version.version
|
||||
if asset.current_published_version_id
|
||||
else None
|
||||
),
|
||||
"versions": [serialize_asset_version(version) for version in versions],
|
||||
"created_at": asset.created_at,
|
||||
"updated_at": asset.updated_at,
|
||||
}
|
||||
|
||||
|
||||
def require_asset_permission(request, asset_type: str, action: str) -> None:
|
||||
module = "solution" if asset_type == GovernedAssetType.SOLUTION else "knowledge"
|
||||
require_permission(request, f"{module}.{action}")
|
||||
|
||||
|
||||
@router.post("/bases", response=KnowledgeBaseOut)
|
||||
def create_knowledge_base(request, payload: KnowledgeBaseIn):
|
||||
require_permission(request, "knowledge.manage")
|
||||
@@ -41,23 +128,341 @@ def list_knowledge_bases(request, enabled: bool | None = None):
|
||||
@router.post("/items", response=KnowledgeItemOut)
|
||||
def create_knowledge_item(request, payload: KnowledgeItemIn):
|
||||
require_permission(request, "knowledge.manage")
|
||||
item = KnowledgeItem.objects.create(
|
||||
knowledge_base_id=payload.knowledge_base_id,
|
||||
title=payload.title,
|
||||
problem=payload.problem,
|
||||
applicable_scope=payload.applicable_scope,
|
||||
error_code=payload.error_code,
|
||||
root_cause=payload.root_cause,
|
||||
troubleshooting_steps=payload.troubleshooting_steps,
|
||||
solution=payload.solution,
|
||||
risk_note=payload.risk_note,
|
||||
status=payload.status,
|
||||
created_by=request.user if request.user.is_authenticated else None,
|
||||
)
|
||||
rebuild_item_chunks(item)
|
||||
category_code = payload.category_code.strip()
|
||||
if category_code and not TicketCategory.objects.filter(
|
||||
code=category_code,
|
||||
enabled=True,
|
||||
).exists():
|
||||
raise HttpError(400, "知识适用分类不存在或已停用")
|
||||
try:
|
||||
with transaction.atomic():
|
||||
item = KnowledgeItem.objects.create(
|
||||
knowledge_base_id=payload.knowledge_base_id,
|
||||
title=payload.title,
|
||||
problem=payload.problem,
|
||||
applicable_scope=payload.applicable_scope,
|
||||
category_code=category_code,
|
||||
related_system=payload.related_system,
|
||||
environment_scope=payload.environment_scope,
|
||||
error_code=payload.error_code,
|
||||
root_cause=payload.root_cause,
|
||||
troubleshooting_steps=payload.troubleshooting_steps,
|
||||
solution=payload.solution,
|
||||
risk_note=payload.risk_note,
|
||||
status="draft",
|
||||
created_by=request.user if request.user.is_authenticated else None,
|
||||
)
|
||||
ensure_governed_asset(
|
||||
GovernedAssetType.KNOWLEDGE_ITEM,
|
||||
item,
|
||||
actor=request.user,
|
||||
)
|
||||
except KnowledgeEmbeddingError as error:
|
||||
raise HttpError(503, str(error)) from error
|
||||
return item
|
||||
|
||||
|
||||
@router.post("/troubleshooting-plans", response=TroubleshootingPlanOut)
|
||||
def create_troubleshooting_plan(request, payload: TroubleshootingPlanIn):
|
||||
require_permission(request, "knowledge.manage")
|
||||
category_code = payload.category_code.strip()
|
||||
if category_code and not TicketCategory.objects.filter(
|
||||
code=category_code,
|
||||
enabled=True,
|
||||
).exists():
|
||||
raise HttpError(400, "排查方案适用分类不存在或已停用")
|
||||
with transaction.atomic():
|
||||
plan = TroubleshootingPlan.objects.create(
|
||||
knowledge_base_id=payload.knowledge_base_id,
|
||||
title=payload.title,
|
||||
problem_pattern=payload.problem_pattern,
|
||||
applicable_scope=payload.applicable_scope,
|
||||
category_code=category_code,
|
||||
related_system=payload.related_system,
|
||||
environment_scope=payload.environment_scope,
|
||||
decision_rules=payload.decision_rules,
|
||||
troubleshooting_steps=payload.troubleshooting_steps,
|
||||
risk_note=payload.risk_note,
|
||||
source_ticket_id=payload.source_ticket_id,
|
||||
created_by=request.user,
|
||||
)
|
||||
ensure_governed_asset(
|
||||
GovernedAssetType.TROUBLESHOOTING_PLAN,
|
||||
plan,
|
||||
actor=request.user,
|
||||
)
|
||||
return plan
|
||||
|
||||
|
||||
@router.get("/troubleshooting-plans", response=list[TroubleshootingPlanOut])
|
||||
def list_troubleshooting_plans(request, status: str | None = None):
|
||||
require_permission(request, "knowledge.view")
|
||||
queryset = TroubleshootingPlan.objects.all()
|
||||
if status:
|
||||
queryset = queryset.filter(status=status)
|
||||
return queryset[:100]
|
||||
|
||||
|
||||
@router.get("/assets", response=list[GovernedAssetOut])
|
||||
def list_governed_assets(
|
||||
request,
|
||||
asset_type: str | None = None,
|
||||
status: str | None = None,
|
||||
):
|
||||
if asset_type:
|
||||
require_asset_permission(request, asset_type, "view")
|
||||
else:
|
||||
require_permission(request, "knowledge.view")
|
||||
require_permission(request, "solution.view")
|
||||
queryset = GovernedAsset.objects.select_related("current_published_version")
|
||||
if asset_type:
|
||||
queryset = queryset.filter(asset_type=asset_type)
|
||||
if status:
|
||||
queryset = queryset.filter(versions__status=status).distinct()
|
||||
return [serialize_governed_asset(asset) for asset in queryset[:200]]
|
||||
|
||||
|
||||
@router.get("/assets/{public_id}", response=GovernedAssetOut)
|
||||
def get_governed_asset(request, public_id: str):
|
||||
asset = GovernedAsset.objects.select_related("current_published_version").filter(
|
||||
public_id=public_id
|
||||
).first()
|
||||
if asset is None:
|
||||
raise HttpError(404, "知识资产不存在")
|
||||
require_asset_permission(request, asset.asset_type, "view")
|
||||
return serialize_governed_asset(asset)
|
||||
|
||||
|
||||
@router.post("/assets/{public_id}/revisions", response=GovernedAssetVersionOut)
|
||||
def create_governed_asset_revision(
|
||||
request,
|
||||
public_id: str,
|
||||
payload: AssetRevisionIn,
|
||||
):
|
||||
asset = GovernedAsset.objects.filter(public_id=public_id).first()
|
||||
if asset is None:
|
||||
raise HttpError(404, "知识资产不存在")
|
||||
require_asset_permission(request, asset.asset_type, "manage")
|
||||
try:
|
||||
return serialize_asset_version(
|
||||
create_asset_revision(
|
||||
asset,
|
||||
actor=request.user,
|
||||
change_summary=payload.change_summary,
|
||||
)
|
||||
)
|
||||
except AssetGovernanceError as error:
|
||||
raise HttpError(400, str(error)) from error
|
||||
|
||||
|
||||
def _asset_version(public_id: str, version_number: int):
|
||||
return (
|
||||
GovernedAssetVersion.objects.select_related("asset", "source_version")
|
||||
.filter(asset__public_id=public_id, version=version_number)
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/assets/{public_id}/versions/{version_number}",
|
||||
response=GovernedAssetVersionOut,
|
||||
)
|
||||
def edit_governed_asset_version(
|
||||
request,
|
||||
public_id: str,
|
||||
version_number: int,
|
||||
payload: AssetVersionUpdateIn,
|
||||
):
|
||||
version = _asset_version(public_id, version_number)
|
||||
if version is None:
|
||||
raise HttpError(404, "资产版本不存在")
|
||||
require_asset_permission(request, version.asset.asset_type, "manage")
|
||||
try:
|
||||
return serialize_asset_version(
|
||||
update_asset_version(
|
||||
version,
|
||||
payload.content,
|
||||
actor=request.user,
|
||||
change_summary=payload.change_summary,
|
||||
)
|
||||
)
|
||||
except AssetGovernanceError as error:
|
||||
raise HttpError(400, str(error)) from error
|
||||
|
||||
|
||||
@router.post(
|
||||
"/assets/{public_id}/versions/{version_number}/submit",
|
||||
response=GovernedAssetVersionOut,
|
||||
)
|
||||
def submit_governed_asset_version(request, public_id: str, version_number: int):
|
||||
version = _asset_version(public_id, version_number)
|
||||
if version is None:
|
||||
raise HttpError(404, "资产版本不存在")
|
||||
require_asset_permission(request, version.asset.asset_type, "submit")
|
||||
try:
|
||||
return serialize_asset_version(
|
||||
submit_asset_version(version, actor=request.user)
|
||||
)
|
||||
except AssetGovernanceError as error:
|
||||
raise HttpError(400, str(error)) from error
|
||||
|
||||
|
||||
@router.post(
|
||||
"/assets/{public_id}/versions/{version_number}/approve",
|
||||
response=GovernedAssetVersionOut,
|
||||
)
|
||||
def approve_governed_asset_version(
|
||||
request,
|
||||
public_id: str,
|
||||
version_number: int,
|
||||
payload: AssetReviewIn,
|
||||
):
|
||||
version = _asset_version(public_id, version_number)
|
||||
if version is None:
|
||||
raise HttpError(404, "资产版本不存在")
|
||||
require_asset_permission(request, version.asset.asset_type, "review")
|
||||
try:
|
||||
return serialize_asset_version(
|
||||
approve_asset_version(
|
||||
version,
|
||||
reviewer=request.user,
|
||||
comment=payload.comment,
|
||||
)
|
||||
)
|
||||
except AssetGovernanceError as error:
|
||||
raise HttpError(400, str(error)) from error
|
||||
|
||||
|
||||
@router.post(
|
||||
"/assets/{public_id}/versions/{version_number}/reject",
|
||||
response=GovernedAssetVersionOut,
|
||||
)
|
||||
def reject_governed_asset_version(
|
||||
request,
|
||||
public_id: str,
|
||||
version_number: int,
|
||||
payload: AssetReviewIn,
|
||||
):
|
||||
version = _asset_version(public_id, version_number)
|
||||
if version is None:
|
||||
raise HttpError(404, "资产版本不存在")
|
||||
require_asset_permission(request, version.asset.asset_type, "review")
|
||||
try:
|
||||
return serialize_asset_version(
|
||||
reject_asset_version(
|
||||
version,
|
||||
reviewer=request.user,
|
||||
comment=payload.comment,
|
||||
)
|
||||
)
|
||||
except AssetGovernanceError as error:
|
||||
raise HttpError(400, str(error)) from error
|
||||
|
||||
|
||||
@router.post("/assets/{public_id}/offline", response=GovernedAssetVersionOut)
|
||||
def offline_governed_asset(
|
||||
request,
|
||||
public_id: str,
|
||||
payload: AssetReviewIn,
|
||||
):
|
||||
asset = GovernedAsset.objects.filter(public_id=public_id).first()
|
||||
if asset is None:
|
||||
raise HttpError(404, "知识资产不存在")
|
||||
require_asset_permission(request, asset.asset_type, "review")
|
||||
try:
|
||||
return serialize_asset_version(
|
||||
offline_asset(asset, actor=request.user, comment=payload.comment)
|
||||
)
|
||||
except AssetGovernanceError as error:
|
||||
raise HttpError(400, str(error)) from error
|
||||
|
||||
|
||||
@router.post(
|
||||
"/assets/{public_id}/rollback",
|
||||
response=GovernedAssetVersionOut,
|
||||
)
|
||||
def rollback_governed_asset(
|
||||
request,
|
||||
public_id: str,
|
||||
payload: AssetRollbackIn,
|
||||
):
|
||||
asset = GovernedAsset.objects.filter(public_id=public_id).first()
|
||||
if asset is None:
|
||||
raise HttpError(404, "知识资产不存在")
|
||||
require_asset_permission(request, asset.asset_type, "manage")
|
||||
source = asset.versions.filter(version=payload.source_version).first()
|
||||
if source is None:
|
||||
raise HttpError(404, "回滚来源版本不存在")
|
||||
try:
|
||||
return serialize_asset_version(
|
||||
rollback_asset(
|
||||
asset,
|
||||
source,
|
||||
actor=request.user,
|
||||
change_summary=payload.change_summary,
|
||||
)
|
||||
)
|
||||
except AssetGovernanceError as error:
|
||||
raise HttpError(400, str(error)) from error
|
||||
|
||||
|
||||
@router.get(
|
||||
"/assets/{public_id}/compare",
|
||||
response=AssetVersionComparisonOut,
|
||||
)
|
||||
def compare_governed_asset_versions(
|
||||
request,
|
||||
public_id: str,
|
||||
left: int,
|
||||
right: int,
|
||||
):
|
||||
asset = GovernedAsset.objects.filter(public_id=public_id).first()
|
||||
if asset is None:
|
||||
raise HttpError(404, "知识资产不存在")
|
||||
require_asset_permission(request, asset.asset_type, "view")
|
||||
left_version = asset.versions.filter(version=left).first()
|
||||
right_version = asset.versions.filter(version=right).first()
|
||||
if left_version is None or right_version is None:
|
||||
raise HttpError(404, "比较版本不存在")
|
||||
return compare_asset_versions(left_version, right_version)
|
||||
|
||||
|
||||
@router.get("/assets/{public_id}/impact", response=AssetImpactOut)
|
||||
def get_governed_asset_impact(request, public_id: str):
|
||||
asset = GovernedAsset.objects.filter(public_id=public_id).first()
|
||||
if asset is None:
|
||||
raise HttpError(404, "知识资产不存在")
|
||||
require_asset_permission(request, asset.asset_type, "view")
|
||||
source_type = (
|
||||
"solution"
|
||||
if asset.asset_type == GovernedAssetType.SOLUTION
|
||||
else "knowledge"
|
||||
)
|
||||
analyses = []
|
||||
for analysis in TicketSupportAnalysis.objects.select_related("run").all()[:1000]:
|
||||
if any(
|
||||
reference.get("source_type") == source_type
|
||||
and reference.get("source_id") == asset.object_id
|
||||
for reference in analysis.references
|
||||
):
|
||||
analyses.append(analysis)
|
||||
ticket_reference_count = len({analysis.ticket_id for analysis in analyses})
|
||||
adoption_count = (
|
||||
TicketSolutionLink.objects.filter(
|
||||
solution_id=asset.object_id,
|
||||
link_type="adopted",
|
||||
).count()
|
||||
if asset.asset_type == GovernedAssetType.SOLUTION
|
||||
else 0
|
||||
)
|
||||
return {
|
||||
"ticket_reference_count": ticket_reference_count,
|
||||
"ticket_adoption_count": adoption_count,
|
||||
"support_analysis_count": len(analyses),
|
||||
"agent_run_ids": [str(analysis.run.public_id) for analysis in analyses[:20]],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/items", response=list[KnowledgeItemOut])
|
||||
def list_knowledge_items(
|
||||
request,
|
||||
@@ -76,20 +481,118 @@ def list_knowledge_items(
|
||||
@router.post("/documents", response=KnowledgeDocumentOut)
|
||||
def create_knowledge_document(request, payload: KnowledgeDocumentIn):
|
||||
require_permission(request, "knowledge.manage")
|
||||
return ingest_document_text(
|
||||
knowledge_base_id=payload.knowledge_base_id,
|
||||
file_name=payload.file_name,
|
||||
file_type=payload.file_type,
|
||||
content=payload.content,
|
||||
operator=request.user if request.user.is_authenticated else None,
|
||||
try:
|
||||
with transaction.atomic():
|
||||
return ingest_document_text(
|
||||
knowledge_base_id=payload.knowledge_base_id,
|
||||
file_name=payload.file_name,
|
||||
file_type=payload.file_type,
|
||||
content=payload.content,
|
||||
operator=request.user if request.user.is_authenticated else None,
|
||||
)
|
||||
except KnowledgeEmbeddingError as error:
|
||||
raise HttpError(503, str(error)) from error
|
||||
|
||||
|
||||
@router.get("/documents", response=list[KnowledgeDocumentOut])
|
||||
def list_knowledge_documents(
|
||||
request,
|
||||
knowledge_base_id: int | None = None,
|
||||
parse_status: str | None = None,
|
||||
):
|
||||
require_permission(request, "knowledge.view")
|
||||
queryset = KnowledgeDocument.objects.all()
|
||||
if knowledge_base_id:
|
||||
queryset = queryset.filter(knowledge_base_id=knowledge_base_id)
|
||||
if parse_status:
|
||||
queryset = queryset.filter(parse_status=parse_status)
|
||||
return queryset[:100]
|
||||
|
||||
|
||||
@router.post("/documents/upload", response=KnowledgeDocumentOut)
|
||||
def upload_knowledge_document(
|
||||
request,
|
||||
knowledge_base_id: int,
|
||||
file: File[UploadedFile],
|
||||
):
|
||||
require_permission(request, "knowledge.manage")
|
||||
if not KnowledgeBase.objects.filter(id=knowledge_base_id).exists():
|
||||
raise HttpError(404, "知识库不存在")
|
||||
raw = file.read()
|
||||
try:
|
||||
prepared = prepare_document(
|
||||
raw,
|
||||
file.name or "document",
|
||||
file.content_type or "",
|
||||
)
|
||||
except MaterialValidationError as error:
|
||||
raise HttpError(error.status_code, str(error)) from error
|
||||
try:
|
||||
document = create_uploaded_document(
|
||||
knowledge_base_id=knowledge_base_id,
|
||||
prepared=prepared,
|
||||
operator=request.user if request.user.is_authenticated else None,
|
||||
)
|
||||
except Exception as error:
|
||||
raise HttpError(503, "知识文档存储暂时不可用") from error
|
||||
dispatch_knowledge_document_processing(document.id)
|
||||
document.refresh_from_db()
|
||||
return document
|
||||
|
||||
|
||||
@router.post("/documents/{public_id}/retry", response=KnowledgeDocumentOut)
|
||||
def retry_knowledge_document(request, public_id: str):
|
||||
require_permission(request, "knowledge.manage")
|
||||
document = KnowledgeDocument.objects.filter(public_id=public_id).first()
|
||||
if document is None:
|
||||
raise HttpError(404, "知识文档不存在")
|
||||
if document.scan_status == "infected":
|
||||
raise HttpError(400, "恶意文件扫描未通过的文档不能重试")
|
||||
updated = KnowledgeDocument.objects.filter(
|
||||
id=document.id,
|
||||
parse_status="failed",
|
||||
).update(
|
||||
scan_status="pending",
|
||||
scan_result={},
|
||||
parse_status="pending",
|
||||
parse_error="",
|
||||
task_id="",
|
||||
)
|
||||
if not updated:
|
||||
raise HttpError(400, "只有处理失败的知识文档可以重试")
|
||||
dispatch_knowledge_document_processing(document.id)
|
||||
document.refresh_from_db()
|
||||
return document
|
||||
|
||||
|
||||
@router.get("/documents/{public_id}/download")
|
||||
def download_knowledge_document(request, public_id: str):
|
||||
require_permission(request, "knowledge.view")
|
||||
document = KnowledgeDocument.objects.filter(public_id=public_id).first()
|
||||
if document is None or not document.storage_path:
|
||||
raise HttpError(404, "知识文档原件不存在")
|
||||
if document.scan_status == "infected":
|
||||
raise HttpError(423, "恶意文件扫描未通过,原件已隔离")
|
||||
try:
|
||||
raw = get_object(document.storage_path)
|
||||
except Exception as error:
|
||||
raise HttpError(503, "知识文档存储暂时不可用") from error
|
||||
response = HttpResponse(raw, content_type=document.mime_type or "application/octet-stream")
|
||||
response["Content-Disposition"] = content_disposition_header(
|
||||
True,
|
||||
document.file_name,
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@router.post("/search", response=list[KnowledgeSearchResultOut])
|
||||
def search_knowledge(request, payload: KnowledgeSearchIn):
|
||||
require_permission(request, "knowledge.view")
|
||||
return retrieve_knowledge(
|
||||
query=payload.query,
|
||||
knowledge_base_id=payload.knowledge_base_id,
|
||||
limit=min(max(payload.limit, 1), 20),
|
||||
)
|
||||
try:
|
||||
return retrieve_knowledge(
|
||||
query=payload.query,
|
||||
knowledge_base_id=payload.knowledge_base_id,
|
||||
limit=min(max(payload.limit, 1), 20),
|
||||
)
|
||||
except KnowledgeEmbeddingError as error:
|
||||
raise HttpError(503, str(error)) from error
|
||||
|
||||
Reference in New Issue
Block a user