mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-20 14:41:05 +08:00
Refactor: Merge document update API (#13962)
### What problem does this PR solve? Refactor: merge document.rename into document.update_document ### Type of change - [x] Refactoring <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a unified document update API (PUT) supporting name, metadata, parser/chunk settings, and status changes. * **Breaking Changes** * Legacy single-parameter rename endpoint removed; renames now require dataset + document identifiers. * `/list` now reads dataset id from a different query parameter. * **Validation / Bug Fixes** * Stricter meta_fields and parser-config validation; unauthenticated requests return 401. * **Frontend** * UI now sends dataset id when saving document names. * **Tests** * Numerous unit and HTTP tests adjusted or removed to match new API and validations. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Jin Hai <haijin.chn@gmail.com> Co-authored-by: Jin Hai <haijin.chn@gmail.com> Co-authored-by: MkDev11 <94194147+MkDev11@users.noreply.github.com> Co-authored-by: mkdev11 <YOUR_GITHUB_ID+MkDev11@users.noreply.github.com> Co-authored-by: mkdev11 <MkDev11@users.noreply.github.com> Co-authored-by: Qi Wang <wangq8@outlook.com> Co-authored-by: dataCenter430 <161712630+dataCenter430@users.noreply.github.com> Co-authored-by: balibabu <cike8899@users.noreply.github.com>
This commit is contained in:
@@ -15,7 +15,6 @@
|
||||
#
|
||||
import json
|
||||
import os.path
|
||||
import pathlib
|
||||
import re
|
||||
from pathlib import Path, PurePosixPath, PureWindowsPath
|
||||
|
||||
@@ -49,7 +48,7 @@ from common.file_utils import get_project_base_directory
|
||||
from common.metadata_utils import convert_conditions, meta_filter, turn2jsonschema
|
||||
from common.misc_utils import get_uuid, thread_pool_exec
|
||||
from deepdoc.parser.html_parser import RAGFlowHtmlParser
|
||||
from rag.nlp import rag_tokenizer, search
|
||||
from rag.nlp import search
|
||||
|
||||
|
||||
def _is_safe_download_filename(name: str) -> bool:
|
||||
@@ -666,61 +665,6 @@ async def run():
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route("/rename", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
@validate_request("doc_id", "name")
|
||||
async def rename():
|
||||
req = await get_request_json()
|
||||
uid = current_user.id
|
||||
try:
|
||||
|
||||
def _rename_sync():
|
||||
if not DocumentService.accessible(req["doc_id"], uid):
|
||||
return get_json_result(data=False, message="No authorization.", code=RetCode.AUTHENTICATION_ERROR)
|
||||
|
||||
e, doc = DocumentService.get_by_id(req["doc_id"])
|
||||
if not e:
|
||||
return get_data_error_result(message="Document not found!")
|
||||
if pathlib.Path(req["name"].lower()).suffix != pathlib.Path(doc.name.lower()).suffix:
|
||||
return get_json_result(data=False, message="The extension of file can't be changed", code=RetCode.ARGUMENT_ERROR)
|
||||
if len(req["name"].encode("utf-8")) > FILE_NAME_LEN_LIMIT:
|
||||
return get_json_result(data=False, message=f"File name must be {FILE_NAME_LEN_LIMIT} bytes or less.", code=RetCode.ARGUMENT_ERROR)
|
||||
|
||||
for d in DocumentService.query(name=req["name"], kb_id=doc.kb_id):
|
||||
if d.name == req["name"]:
|
||||
return get_data_error_result(message="Duplicated document name in the same dataset.")
|
||||
|
||||
if not DocumentService.update_by_id(req["doc_id"], {"name": req["name"]}):
|
||||
return get_data_error_result(message="Database error (Document rename)!")
|
||||
|
||||
informs = File2DocumentService.get_by_document_id(req["doc_id"])
|
||||
if informs:
|
||||
e, file = FileService.get_by_id(informs[0].file_id)
|
||||
FileService.update_by_id(file.id, {"name": req["name"]})
|
||||
|
||||
tenant_id = DocumentService.get_tenant_id(req["doc_id"])
|
||||
title_tks = rag_tokenizer.tokenize(req["name"])
|
||||
es_body = {
|
||||
"docnm_kwd": req["name"],
|
||||
"title_tks": title_tks,
|
||||
"title_sm_tks": rag_tokenizer.fine_grained_tokenize(title_tks),
|
||||
}
|
||||
if settings.docStoreConn.index_exist(search.index_name(tenant_id), doc.kb_id):
|
||||
settings.docStoreConn.update(
|
||||
{"doc_id": req["doc_id"]},
|
||||
es_body,
|
||||
search.index_name(tenant_id),
|
||||
doc.kb_id,
|
||||
)
|
||||
return get_json_result(data=True)
|
||||
|
||||
return await thread_pool_exec(_rename_sync)
|
||||
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route("/get/<doc_id>", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
async def get(doc_id):
|
||||
|
||||
146
api/apps/restful_apis/document_api.py
Normal file
146
api/apps/restful_apis/document_api.py
Normal file
@@ -0,0 +1,146 @@
|
||||
#
|
||||
# Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
import logging
|
||||
|
||||
from peewee import OperationalError
|
||||
from pydantic import ValidationError
|
||||
|
||||
from api.apps.services.document_api_service import rename_doc_key, validate_document_update_fields, \
|
||||
update_document_name_only, update_chunk_method_only, update_document_status_only
|
||||
from api.db.services.doc_metadata_service import DocMetadataService
|
||||
from api.db.services.document_service import DocumentService
|
||||
from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
from common.constants import RetCode
|
||||
from api.apps import login_required
|
||||
from api.utils.api_utils import get_error_data_result, get_result, add_tenant_id_to_kwargs, get_request_json
|
||||
from api.utils.validation_utils import (
|
||||
UpdateDocumentReq, format_validation_error_message,
|
||||
)
|
||||
|
||||
@manager.route("/datasets/<dataset_id>/documents/<document_id>", methods=["PUT"]) # noqa: F821
|
||||
@login_required
|
||||
@add_tenant_id_to_kwargs
|
||||
async def update_document(tenant_id, dataset_id, document_id):
|
||||
"""
|
||||
Update a document within a dataset.
|
||||
---
|
||||
tags:
|
||||
- Documents
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: dataset_id
|
||||
type: string
|
||||
required: true
|
||||
description: ID of the dataset.
|
||||
- in: path
|
||||
name: document_id
|
||||
type: string
|
||||
required: true
|
||||
description: ID of the document to update.
|
||||
- in: header
|
||||
name: Authorization
|
||||
type: string
|
||||
required: true
|
||||
description: Bearer token for authentication.
|
||||
- in: body
|
||||
name: body
|
||||
description: Document update parameters.
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: New name of the document.
|
||||
parser_config:
|
||||
type: object
|
||||
description: Parser configuration.
|
||||
chunk_method:
|
||||
type: string
|
||||
description: Chunking method.
|
||||
enabled:
|
||||
type: boolean
|
||||
description: Document status.
|
||||
responses:
|
||||
200:
|
||||
description: Document updated successfully.
|
||||
schema:
|
||||
type: object
|
||||
"""
|
||||
req = await get_request_json()
|
||||
|
||||
# Verify ownership and existence of dataset and document
|
||||
if not KnowledgebaseService.query(id=dataset_id, tenant_id=tenant_id):
|
||||
return get_error_data_result(message="You don't own the dataset.")
|
||||
e, kb = KnowledgebaseService.get_by_id(dataset_id)
|
||||
if not e:
|
||||
return get_error_data_result(message="Can't find this dataset!")
|
||||
|
||||
# Prepare data for validation
|
||||
docs = DocumentService.query(kb_id=dataset_id, id=document_id)
|
||||
if not docs:
|
||||
return get_error_data_result(message="The dataset doesn't own the document.")
|
||||
|
||||
# Validate document update request parameters
|
||||
try:
|
||||
update_doc_req = UpdateDocumentReq(**req)
|
||||
except ValidationError as e:
|
||||
return get_error_data_result(message=format_validation_error_message(e), code=RetCode.DATA_ERROR)
|
||||
|
||||
doc = docs[0]
|
||||
|
||||
# further check with inner status (from DB)
|
||||
error_msg, error_code = validate_document_update_fields(update_doc_req, doc, req)
|
||||
if error_msg:
|
||||
return get_error_data_result(message=error_msg, code=error_code)
|
||||
|
||||
# All validations passed, now perform all updates
|
||||
# meta_fields provided, then update it
|
||||
if "meta_fields" in req:
|
||||
if not DocMetadataService.update_document_metadata(document_id, update_doc_req.meta_fields):
|
||||
return get_error_data_result(message="Failed to update metadata")
|
||||
# doc name provided from request and diff with existing value, update
|
||||
if "name" in req and req["name"] != doc.name:
|
||||
if error := update_document_name_only(document_id, req["name"]):
|
||||
return error
|
||||
|
||||
# parser config provided (already validated in UpdateDocumentReq), update it
|
||||
if update_doc_req.parser_config:
|
||||
DocumentService.update_parser_config(doc.id, req["parser_config"])
|
||||
|
||||
# chunk method provided - the update method will check if it's different with existing one
|
||||
if update_doc_req.chunk_method:
|
||||
if error := update_chunk_method_only(req, doc, dataset_id, tenant_id):
|
||||
return error
|
||||
|
||||
if "enabled" in req: # already checked in UpdateDocumentReq - it's int if it's present
|
||||
# "enabled" flag provided, the update method will check if it's changed and then update if so
|
||||
if error := update_document_status_only(int(req["enabled"]), doc, kb):
|
||||
return error
|
||||
|
||||
try:
|
||||
original_doc_id = doc.id
|
||||
ok, doc = DocumentService.get_by_id(doc.id)
|
||||
if not ok:
|
||||
return get_error_data_result(message=f"Can not get document by id:{original_doc_id}")
|
||||
except OperationalError as e:
|
||||
logging.exception(e)
|
||||
return get_error_data_result(message="Database operation failed")
|
||||
renamed_doc = rename_doc_key(doc)
|
||||
return get_result(data=renamed_doc)
|
||||
|
||||
@@ -15,13 +15,11 @@
|
||||
#
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from io import BytesIO
|
||||
|
||||
import xxhash
|
||||
from peewee import OperationalError
|
||||
from pydantic import BaseModel, Field, validator, ValidationError
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from quart import request, send_file
|
||||
|
||||
from api.constants import FILE_NAME_LEN_LIMIT
|
||||
@@ -35,10 +33,8 @@ from api.db.services.knowledgebase_service import KnowledgebaseService
|
||||
from api.db.services.llm_service import LLMBundle
|
||||
from api.db.services.task_service import TaskService, cancel_all_task_of, queue_tasks
|
||||
from api.db.services.tenant_llm_service import TenantLLMService
|
||||
from api.utils import validation_utils
|
||||
from api.utils.api_utils import check_duplicate_ids, construct_json_result, get_error_data_result, get_parser_config, get_request_json, get_result, server_error_response, token_required
|
||||
from api.utils.api_utils import check_duplicate_ids, construct_json_result, get_error_data_result, get_request_json, get_result, server_error_response, token_required
|
||||
from api.utils.image_utils import store_chunk_image
|
||||
from api.utils.validation_utils import format_validation_error_message, UpdateDocumentReq
|
||||
from common import settings
|
||||
from common.constants import FileSource, LLMType, ParserType, RetCode, TaskStatus
|
||||
from common.metadata_utils import convert_conditions, meta_filter
|
||||
@@ -184,215 +180,6 @@ async def upload(dataset_id, tenant_id):
|
||||
renamed_doc_list.append(renamed_doc)
|
||||
return get_result(data=renamed_doc_list)
|
||||
|
||||
|
||||
def _update_document_name_only(document_id, req_doc_name):
|
||||
"""Update document name only (without validation)."""
|
||||
if not DocumentService.update_by_id(document_id, {"name": req_doc_name}):
|
||||
return get_error_data_result(message="Database error (Document rename)!")
|
||||
|
||||
informs = File2DocumentService.get_by_document_id(document_id)
|
||||
if informs:
|
||||
e, file = FileService.get_by_id(informs[0].file_id)
|
||||
FileService.update_by_id(file.id, {"name": req_doc_name})
|
||||
return None
|
||||
|
||||
def _update_chunk_method_only(req, doc, dataset_id, tenant_id):
|
||||
"""Update chunk method only (without validation)."""
|
||||
if doc.parser_id.lower() != req["chunk_method"].lower():
|
||||
# if chunk method changed
|
||||
e = DocumentService.update_by_id(
|
||||
doc.id,
|
||||
{
|
||||
"parser_id": req["chunk_method"],
|
||||
"progress": 0,
|
||||
"progress_msg": "",
|
||||
"run": TaskStatus.UNSTART.value,
|
||||
},
|
||||
)
|
||||
if not e:
|
||||
return get_error_data_result(message="Document not found!")
|
||||
if not req.get("parser_config"):
|
||||
req["parser_config"] = get_parser_config(req["chunk_method"], req.get("parser_config"))
|
||||
DocumentService.update_parser_config(doc.id, req["parser_config"])
|
||||
if doc.token_num > 0:
|
||||
e = DocumentService.increment_chunk_num(
|
||||
doc.id,
|
||||
doc.kb_id,
|
||||
doc.token_num * -1,
|
||||
doc.chunk_num * -1,
|
||||
doc.process_duration * -1,
|
||||
)
|
||||
if not e:
|
||||
return get_error_data_result(message="Document not found!")
|
||||
settings.docStoreConn.delete({"doc_id": doc.id}, search.index_name(tenant_id), dataset_id)
|
||||
return None
|
||||
|
||||
def _update_document_status_only(status:int, doc, kb):
|
||||
"""Update document status only (without validation)."""
|
||||
if doc.status is None or (int(doc.status) != status):
|
||||
try:
|
||||
if not DocumentService.update_by_id(doc.id, {"status": str(status)}):
|
||||
return get_error_data_result(message="Database error (Document update)!")
|
||||
settings.docStoreConn.update({"doc_id": doc.id}, {"available_int": status}, search.index_name(kb.tenant_id), doc.kb_id)
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
return None
|
||||
|
||||
|
||||
@manager.route("/datasets/<dataset_id>/documents/<document_id>", methods=["PUT"]) # noqa: F821
|
||||
@token_required
|
||||
async def update_doc(tenant_id, dataset_id, document_id):
|
||||
"""
|
||||
Update a document within a dataset.
|
||||
---
|
||||
tags:
|
||||
- Documents
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: dataset_id
|
||||
type: string
|
||||
required: true
|
||||
description: ID of the dataset.
|
||||
- in: path
|
||||
name: document_id
|
||||
type: string
|
||||
required: true
|
||||
description: ID of the document to update.
|
||||
- in: header
|
||||
name: Authorization
|
||||
type: string
|
||||
required: true
|
||||
description: Bearer token for authentication.
|
||||
- in: body
|
||||
name: body
|
||||
description: Document update parameters.
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: New name of the document.
|
||||
parser_config:
|
||||
type: object
|
||||
description: Parser configuration.
|
||||
chunk_method:
|
||||
type: string
|
||||
description: Chunking method.
|
||||
enabled:
|
||||
type: boolean
|
||||
description: Document status.
|
||||
responses:
|
||||
200:
|
||||
description: Document updated successfully.
|
||||
schema:
|
||||
type: object
|
||||
"""
|
||||
req = await get_request_json()
|
||||
|
||||
# Verify ownership and existence of dataset and document
|
||||
if not KnowledgebaseService.query(id=dataset_id, tenant_id=tenant_id):
|
||||
return get_error_data_result(message="You don't own the dataset.")
|
||||
e, kb = KnowledgebaseService.get_by_id(dataset_id)
|
||||
if not e:
|
||||
return get_error_data_result(message="Can't find this dataset!")
|
||||
|
||||
# Prepare data for validation
|
||||
docs = DocumentService.query(kb_id=dataset_id, id=document_id)
|
||||
if not docs:
|
||||
return get_error_data_result(message="The dataset doesn't own the document.")
|
||||
|
||||
# Validate document update request parameters
|
||||
try:
|
||||
update_doc_req = UpdateDocumentReq(**req)
|
||||
except ValidationError as e:
|
||||
return get_error_data_result(message=format_validation_error_message(e), code=RetCode.DATA_ERROR)
|
||||
|
||||
doc = docs[0]
|
||||
|
||||
# further check with inner status (from DB)
|
||||
error_msg, error_code = _validate_document_update_fields(update_doc_req, doc, req)
|
||||
if error_msg:
|
||||
return get_error_data_result(message=error_msg, code=error_code)
|
||||
|
||||
# All validations passed, now perform all updates
|
||||
# meta_fields provided, then update it
|
||||
if update_doc_req.meta_fields:
|
||||
if not DocMetadataService.update_document_metadata(document_id, update_doc_req.meta_fields):
|
||||
return get_error_data_result(message="Failed to update metadata")
|
||||
# doc name provided from request and diff with existing value, update
|
||||
if "name" in req and req["name"] != doc.name:
|
||||
if error := _update_document_name_only(document_id, req["name"]):
|
||||
return error
|
||||
|
||||
# parser config provided (already validated in UpdateDocumentReq), update it
|
||||
if update_doc_req.parser_config:
|
||||
DocumentService.update_parser_config(doc.id, req["parser_config"])
|
||||
|
||||
# chunk method provided - the update method will check if it's different with existing one
|
||||
if update_doc_req.chunk_method:
|
||||
if error := _update_chunk_method_only(req, doc, dataset_id, tenant_id):
|
||||
return error
|
||||
|
||||
if "enabled" in req: # already checked in UpdateDocumentReq - it's int if it's present
|
||||
# "enabled" flag provided, the update method will check if it's changed and then update if so
|
||||
if error := _update_document_status_only(int(req["enabled"]), doc, kb):
|
||||
return error
|
||||
|
||||
try:
|
||||
ok, doc = DocumentService.get_by_id(doc.id)
|
||||
if not ok:
|
||||
return get_error_data_result(message="Dataset created failed")
|
||||
except OperationalError as e:
|
||||
logging.exception(e)
|
||||
return get_error_data_result(message="Database operation failed")
|
||||
|
||||
key_mapping = {
|
||||
"chunk_num": "chunk_count",
|
||||
"kb_id": "dataset_id",
|
||||
"token_num": "token_count",
|
||||
"parser_id": "chunk_method",
|
||||
}
|
||||
run_mapping = {
|
||||
"0": "UNSTART",
|
||||
"1": "RUNNING",
|
||||
"2": "CANCEL",
|
||||
"3": "DONE",
|
||||
"4": "FAIL",
|
||||
}
|
||||
renamed_doc = {}
|
||||
for key, value in doc.to_dict().items():
|
||||
new_key = key_mapping.get(key, key)
|
||||
renamed_doc[new_key] = value
|
||||
if key == "run":
|
||||
renamed_doc["run"] = run_mapping.get(str(value))
|
||||
|
||||
return get_result(data=renamed_doc)
|
||||
|
||||
def _validate_document_update_fields(update_doc_req:UpdateDocumentReq, doc, req):
|
||||
"""Validate document update fields in a single method."""
|
||||
# Validate immutable fields
|
||||
error_msg, error_code = validation_utils.validate_immutable_fields(update_doc_req, doc)
|
||||
if error_msg:
|
||||
return error_msg, error_code
|
||||
|
||||
# Validate document name if present
|
||||
if "name" in req and req["name"] != doc.name:
|
||||
docs_from_name = DocumentService.query(name=req["name"], kb_id=doc.kb_id)
|
||||
error_msg, error_code = validation_utils.validate_document_name(req["name"], doc, docs_from_name)
|
||||
if error_msg:
|
||||
return error_msg, error_code
|
||||
|
||||
# Validate chunk method if present
|
||||
if "chunk_method" in req:
|
||||
error_msg, error_code = validation_utils.validate_chunk_method(doc, req["chunk_method"])
|
||||
if error_msg:
|
||||
return error_msg, error_code
|
||||
|
||||
return None, None
|
||||
|
||||
@manager.route("/datasets/<dataset_id>/documents/<document_id>", methods=["GET"]) # noqa: F821
|
||||
@token_required
|
||||
async def download(tenant_id, dataset_id, document_id):
|
||||
|
||||
201
api/apps/services/document_api_service.py
Normal file
201
api/apps/services/document_api_service.py
Normal file
@@ -0,0 +1,201 @@
|
||||
#
|
||||
# Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
from api.db.services.document_service import DocumentService
|
||||
from api.db.services.file2document_service import File2DocumentService
|
||||
from api.db.services.file_service import FileService
|
||||
from api.utils import validation_utils
|
||||
from common import settings
|
||||
from common.constants import TaskStatus
|
||||
from api.utils.api_utils import get_error_data_result, server_error_response, get_parser_config
|
||||
from api.utils.validation_utils import UpdateDocumentReq
|
||||
from rag.nlp import rag_tokenizer, search
|
||||
|
||||
|
||||
def update_document_name_only(document_id, req_doc_name):
|
||||
"""
|
||||
Update document name only (without validation).
|
||||
:param document_id: id (string) of the document
|
||||
:param req_doc_name: new name (string) from request for the document
|
||||
:return: None if all are good; otherwise returns the error message in the JSON format
|
||||
"""
|
||||
if not DocumentService.update_by_id(document_id, {"name": req_doc_name}):
|
||||
return get_error_data_result(message="Database error (Document rename)!")
|
||||
|
||||
informs = File2DocumentService.get_by_document_id(document_id)
|
||||
if informs:
|
||||
e, file = FileService.get_by_id(informs[0].file_id)
|
||||
FileService.update_by_id(file.id, {"name": req_doc_name})
|
||||
# Add logic to update index - refer to rename method in document_app.py
|
||||
tenant_id = DocumentService.get_tenant_id(document_id)
|
||||
title_tks = rag_tokenizer.tokenize(req_doc_name)
|
||||
es_body = {
|
||||
"docnm_kwd": req_doc_name,
|
||||
"title_tks": title_tks,
|
||||
"title_sm_tks": rag_tokenizer.fine_grained_tokenize(title_tks),
|
||||
}
|
||||
ok, doc = DocumentService.get_by_id(document_id)
|
||||
if not ok:
|
||||
return get_error_data_result(message=f"Not able to find document by id:{document_id}")
|
||||
if settings.docStoreConn.index_exist(search.index_name(tenant_id), doc.kb_id):
|
||||
settings.docStoreConn.update(
|
||||
{"doc_id": document_id},
|
||||
es_body,
|
||||
search.index_name(tenant_id),
|
||||
doc.kb_id,
|
||||
)
|
||||
return None
|
||||
|
||||
def update_chunk_method_only(req, doc, dataset_id, tenant_id):
|
||||
"""
|
||||
Update chunk method only (without validation).
|
||||
|
||||
Updates the chunk method and parser configuration for a document,
|
||||
and resets the document's progress if the chunk method changes.
|
||||
Also clears existing chunks from the document store if the method changes.
|
||||
|
||||
Args:
|
||||
req: The request dictionary containing chunk_method and parser_config.
|
||||
doc: The document model from the database.
|
||||
dataset_id: The ID of the dataset containing the document.
|
||||
tenant_id: The tenant ID for the document store.
|
||||
|
||||
Returns:
|
||||
None if successful, or an error result dictionary if failed.
|
||||
"""
|
||||
if doc.parser_id.lower() != req["chunk_method"].lower():
|
||||
# if chunk method changed
|
||||
e = DocumentService.update_by_id(
|
||||
doc.id,
|
||||
{
|
||||
"parser_id": req["chunk_method"],
|
||||
"progress": 0,
|
||||
"progress_msg": "",
|
||||
"run": TaskStatus.UNSTART.value,
|
||||
},
|
||||
)
|
||||
if not e:
|
||||
return get_error_data_result(message="Document not found!")
|
||||
if not req.get("parser_config"):
|
||||
req["parser_config"] = get_parser_config(req["chunk_method"], req.get("parser_config"))
|
||||
DocumentService.update_parser_config(doc.id, req["parser_config"])
|
||||
if doc.token_num > 0:
|
||||
e = DocumentService.increment_chunk_num(
|
||||
doc.id,
|
||||
doc.kb_id,
|
||||
doc.token_num * -1,
|
||||
doc.chunk_num * -1,
|
||||
doc.process_duration * -1,
|
||||
)
|
||||
if not e:
|
||||
return get_error_data_result(message="Document not found!")
|
||||
settings.docStoreConn.delete({"doc_id": doc.id}, search.index_name(tenant_id), dataset_id)
|
||||
return None
|
||||
|
||||
def update_document_status_only(status:int, doc, kb):
|
||||
"""
|
||||
Update document status only (without validation).
|
||||
|
||||
Updates the enabled/disabled status of a document and updates
|
||||
the corresponding index in the document store.
|
||||
|
||||
Args:
|
||||
status: The new status value (0 for disabled, 1 for enabled).
|
||||
doc: The document model from the database.
|
||||
kb: The knowledge base model.
|
||||
|
||||
Returns:
|
||||
None if successful, or an error result dictionary if failed.
|
||||
"""
|
||||
if doc.status is None or (int(doc.status) != status):
|
||||
try:
|
||||
if not DocumentService.update_by_id(doc.id, {"status": str(status)}):
|
||||
return get_error_data_result(message="Database error (Document update)!")
|
||||
settings.docStoreConn.update({"doc_id": doc.id}, {"available_int": status}, search.index_name(kb.tenant_id), doc.kb_id)
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
return None
|
||||
|
||||
|
||||
def validate_document_update_fields(update_doc_req:UpdateDocumentReq, doc, req):
|
||||
"""
|
||||
Validate document update fields in a single method.
|
||||
|
||||
Performs comprehensive validation of all document update fields,
|
||||
including immutable fields, document name, and chunk method.
|
||||
|
||||
Args:
|
||||
update_doc_req: The validated update document request.
|
||||
doc: The document model from the database.
|
||||
req: The original request dictionary.
|
||||
|
||||
Returns:
|
||||
A tuple of (error_message, error_code) if validation fails,
|
||||
or (None, None) if validation passes.
|
||||
"""
|
||||
# Validate immutable fields
|
||||
error_msg, error_code = validation_utils.validate_immutable_fields(update_doc_req, doc)
|
||||
if error_msg:
|
||||
return error_msg, error_code
|
||||
|
||||
# Validate document name if present
|
||||
if "name" in req and req["name"] != doc.name:
|
||||
docs_from_name = DocumentService.query(name=req["name"], kb_id=doc.kb_id)
|
||||
error_msg, error_code = validation_utils.validate_document_name(req["name"], doc, docs_from_name)
|
||||
if error_msg:
|
||||
return error_msg, error_code
|
||||
|
||||
# Validate chunk method if present
|
||||
if "chunk_method" in req:
|
||||
error_msg, error_code = validation_utils.validate_chunk_method(doc, req["chunk_method"])
|
||||
if error_msg:
|
||||
return error_msg, error_code
|
||||
|
||||
return None, None
|
||||
|
||||
def rename_doc_key(doc):
|
||||
"""
|
||||
Rename document keys to match API response format.
|
||||
|
||||
Converts internal document model field names to the external API
|
||||
response field names (e.g., 'chunk_num' -> 'chunk_count').
|
||||
|
||||
Args:
|
||||
doc: The document model from the database.
|
||||
|
||||
Returns:
|
||||
A dictionary with renamed keys for API response.
|
||||
"""
|
||||
key_mapping = {
|
||||
"chunk_num": "chunk_count",
|
||||
"kb_id": "dataset_id",
|
||||
"token_num": "token_count",
|
||||
"parser_id": "chunk_method",
|
||||
}
|
||||
run_mapping = {
|
||||
"0": "UNSTART",
|
||||
"1": "RUNNING",
|
||||
"2": "CANCEL",
|
||||
"3": "DONE",
|
||||
"4": "FAIL",
|
||||
}
|
||||
renamed_doc = {}
|
||||
for key, value in doc.to_dict().items():
|
||||
new_key = key_mapping.get(key, key)
|
||||
renamed_doc[new_key] = value
|
||||
if key == "run":
|
||||
renamed_doc["run"] = run_mapping.get(str(value))
|
||||
return renamed_doc
|
||||
|
||||
@@ -402,7 +402,14 @@ class ParserConfig(Base):
|
||||
ext: Annotated[dict, Field(default={})]
|
||||
|
||||
class UpdateDocumentReq(Base):
|
||||
"""
|
||||
Request model for updating a document.
|
||||
|
||||
This model validates the request parameters for updating a document,
|
||||
including name, chunk method, enabled status, and other metadata.
|
||||
"""
|
||||
model_config = ConfigDict(extra='ignore')
|
||||
name: Annotated[str | None, Field(default=None, max_length=65535)]
|
||||
chunk_method: Annotated[str | None, Field(default=None, max_length=65535)]
|
||||
enabled: Annotated[int | None, Field(default=None, ge=0, le=1)]
|
||||
chunk_count: Annotated[int | None, Field(default=None, ge=0)]
|
||||
@@ -432,6 +439,22 @@ class UpdateDocumentReq(Base):
|
||||
|
||||
return enabled
|
||||
|
||||
@field_validator("meta_fields", mode="after")
|
||||
@classmethod
|
||||
def validate_document_meta_fields(cls, meta_fields: dict | None):
|
||||
if meta_fields is None:
|
||||
return None
|
||||
|
||||
if not isinstance(meta_fields, dict):
|
||||
raise PydanticCustomError("format_invalid", "Only dictionary type supported")
|
||||
for k, v in meta_fields.items():
|
||||
if isinstance(v, list):
|
||||
if not all(isinstance(i, (str, int, float)) for i in v):
|
||||
raise PydanticCustomError("format_invalid", "The type is not supported in list: {v}", {"v":v})
|
||||
elif not isinstance(v, (str, int, float)):
|
||||
raise PydanticCustomError("format_invalid", "The type is not supported: {v}", {"v":v})
|
||||
return meta_fields
|
||||
|
||||
class CreateDatasetReq(Base):
|
||||
name: Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=DATASET_NAME_LIMIT), Field(...)]
|
||||
avatar: Annotated[str | None, Field(default=None, max_length=65535)]
|
||||
@@ -854,7 +877,20 @@ class ListFileReq(BaseModel):
|
||||
|
||||
|
||||
def validate_immutable_fields(update_doc_req:UpdateDocumentReq, doc):
|
||||
"""Validate that immutable fields have not been changed."""
|
||||
"""
|
||||
Validate that immutable fields have not been changed.
|
||||
|
||||
Checks that fields like chunk_count, token_count, and progress
|
||||
cannot be modified directly by the user.
|
||||
|
||||
Args:
|
||||
update_doc_req: The validated update document request.
|
||||
doc: The document model from the database.
|
||||
|
||||
Returns:
|
||||
A tuple of (error_message, error_code) if validation fails,
|
||||
or (None, None) if validation passes.
|
||||
"""
|
||||
if update_doc_req.chunk_count and update_doc_req.chunk_count != int(getattr(doc, "chunk_num", -1)):
|
||||
return "Can't change `chunk_count`.", RetCode.DATA_ERROR
|
||||
|
||||
@@ -871,7 +907,24 @@ def validate_immutable_fields(update_doc_req:UpdateDocumentReq, doc):
|
||||
|
||||
|
||||
def validate_document_name(req_doc_name:str, doc, docs_from_name):
|
||||
"""Validate document name update."""
|
||||
"""
|
||||
Validate document name update.
|
||||
|
||||
Checks that the new document name is valid:
|
||||
- Must be a string
|
||||
- Must not exceed the file name length limit
|
||||
- File extension cannot be changed
|
||||
- Must not duplicate an existing document name in the same dataset.
|
||||
|
||||
Args:
|
||||
req_doc_name: The new document name to validate.
|
||||
doc: The document model from the database.
|
||||
docs_from_name: Query result for documents with the new name.
|
||||
|
||||
Returns:
|
||||
A tuple of (error_message, error_code) if validation fails,
|
||||
or (None, None) if validation passes.
|
||||
"""
|
||||
if not isinstance(req_doc_name, str):
|
||||
return f"AttributeError('{type(req_doc_name).__name__}' object has no attribute 'encode')", RetCode.EXCEPTION_ERROR
|
||||
if len(req_doc_name.encode("utf-8")) > FILE_NAME_LEN_LIMIT:
|
||||
@@ -885,7 +938,20 @@ def validate_document_name(req_doc_name:str, doc, docs_from_name):
|
||||
return None, None
|
||||
|
||||
def validate_chunk_method(doc, chunk_method=None):
|
||||
"""Validate chunk method update."""
|
||||
"""
|
||||
Validate chunk method update.
|
||||
|
||||
Checks if the chunk method is valid for the given document,
|
||||
particularly for visual documents or specific file types.
|
||||
|
||||
Args:
|
||||
doc: The document model from the database.
|
||||
chunk_method: The chunk method to validate.
|
||||
|
||||
Returns:
|
||||
A tuple of (error_message, error_code) if validation fails,
|
||||
or (None, None) if validation passes.
|
||||
"""
|
||||
if chunk_method is not None and len(chunk_method) == 0: # will not be detected in UpdateDocumentReq
|
||||
return "`chunk_method` (empty string) is not valid", RetCode.DATA_ERROR
|
||||
if doc.type == FileType.VISUAL or re.search(r"\.(ppt|pptx|pages)$", doc.name):
|
||||
|
||||
Reference in New Issue
Block a user