mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-23 00:46:42 +08:00
Refactor: Consolidation WEB API & HTTP API for document list_docs (#14176)
### What problem does this PR solve? Before consolidation Web API: POST /v1/document/list Http API - GET /api/v1/datasets/<dataset_id>/documents After consolidation, Restful API -- GET /api/v1/datasets/<dataset_id>/documents ### Type of change - [x] Refactoring
This commit is contained in:
@@ -44,7 +44,6 @@ from api.utils.web_utils import CONTENT_TYPE_MAP, apply_safe_file_response_heade
|
||||
from common import settings
|
||||
from common.constants import SANDBOX_ARTIFACT_BUCKET, VALID_TASK_STATUS, ParserType, RetCode, TaskStatus
|
||||
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 search
|
||||
@@ -185,139 +184,6 @@ async def create():
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route("/list", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
async def list_docs():
|
||||
kb_id = request.args.get("id")
|
||||
if not kb_id:
|
||||
return get_json_result(data=False, message='Dataset ID is required for listing files.', code=RetCode.ARGUMENT_ERROR)
|
||||
tenants = UserTenantService.query(user_id=current_user.id)
|
||||
for tenant in tenants:
|
||||
if KnowledgebaseService.query(tenant_id=tenant.tenant_id, id=kb_id):
|
||||
break
|
||||
else:
|
||||
return get_json_result(data=False, message="Only owner of dataset authorized for this operation.", code=RetCode.OPERATING_ERROR)
|
||||
keywords = request.args.get("keywords", "")
|
||||
|
||||
page_number = int(request.args.get("page", 0))
|
||||
items_per_page = int(request.args.get("page_size", 0))
|
||||
orderby = request.args.get("orderby", "create_time")
|
||||
if request.args.get("desc", "true").lower() == "false":
|
||||
desc = False
|
||||
else:
|
||||
desc = True
|
||||
create_time_from = int(request.args.get("create_time_from", 0))
|
||||
create_time_to = int(request.args.get("create_time_to", 0))
|
||||
|
||||
req = await get_request_json()
|
||||
|
||||
return_empty_metadata = req.get("return_empty_metadata", False)
|
||||
if isinstance(return_empty_metadata, str):
|
||||
return_empty_metadata = return_empty_metadata.lower() == "true"
|
||||
|
||||
run_status = req.get("run_status", [])
|
||||
if run_status:
|
||||
invalid_status = {s for s in run_status if s not in VALID_TASK_STATUS}
|
||||
if invalid_status:
|
||||
return get_data_error_result(message=f"Invalid filter run status conditions: {', '.join(invalid_status)}")
|
||||
|
||||
types = req.get("types", [])
|
||||
if types:
|
||||
invalid_types = {t for t in types if t not in VALID_FILE_TYPES}
|
||||
if invalid_types:
|
||||
return get_data_error_result(message=f"Invalid filter conditions: {', '.join(invalid_types)} type{'s' if len(invalid_types) > 1 else ''}")
|
||||
|
||||
suffix = req.get("suffix", [])
|
||||
metadata_condition = req.get("metadata_condition", {}) or {}
|
||||
metadata = req.get("metadata", {}) or {}
|
||||
if isinstance(metadata, dict) and metadata.get("empty_metadata"):
|
||||
return_empty_metadata = True
|
||||
metadata = {k: v for k, v in metadata.items() if k != "empty_metadata"}
|
||||
if return_empty_metadata:
|
||||
metadata_condition = {}
|
||||
metadata = {}
|
||||
else:
|
||||
if metadata_condition and not isinstance(metadata_condition, dict):
|
||||
return get_data_error_result(message="metadata_condition must be an object.")
|
||||
if metadata and not isinstance(metadata, dict):
|
||||
return get_data_error_result(message="metadata must be an object.")
|
||||
|
||||
doc_ids_filter = None
|
||||
metas = None
|
||||
if metadata_condition or metadata:
|
||||
metas = DocMetadataService.get_flatted_meta_by_kbs([kb_id])
|
||||
|
||||
if metadata_condition:
|
||||
doc_ids_filter = set(meta_filter(metas, convert_conditions(metadata_condition), metadata_condition.get("logic", "and")))
|
||||
if metadata_condition.get("conditions") and not doc_ids_filter:
|
||||
return get_json_result(data={"total": 0, "docs": []})
|
||||
|
||||
if metadata:
|
||||
metadata_doc_ids = None
|
||||
for key, values in metadata.items():
|
||||
if not values:
|
||||
continue
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
values = [str(v) for v in values if v is not None and str(v).strip()]
|
||||
if not values:
|
||||
continue
|
||||
key_doc_ids = set()
|
||||
for value in values:
|
||||
key_doc_ids.update(metas.get(key, {}).get(value, []))
|
||||
if metadata_doc_ids is None:
|
||||
metadata_doc_ids = key_doc_ids
|
||||
else:
|
||||
metadata_doc_ids &= key_doc_ids
|
||||
if not metadata_doc_ids:
|
||||
return get_json_result(data={"total": 0, "docs": []})
|
||||
if metadata_doc_ids is not None:
|
||||
if doc_ids_filter is None:
|
||||
doc_ids_filter = metadata_doc_ids
|
||||
else:
|
||||
doc_ids_filter &= metadata_doc_ids
|
||||
if not doc_ids_filter:
|
||||
return get_json_result(data={"total": 0, "docs": []})
|
||||
|
||||
if doc_ids_filter is not None:
|
||||
doc_ids_filter = list(doc_ids_filter)
|
||||
|
||||
try:
|
||||
docs, tol = DocumentService.get_by_kb_id(
|
||||
kb_id,
|
||||
page_number,
|
||||
items_per_page,
|
||||
orderby,
|
||||
desc,
|
||||
keywords,
|
||||
run_status,
|
||||
types,
|
||||
suffix,
|
||||
doc_ids_filter,
|
||||
return_empty_metadata=return_empty_metadata,
|
||||
)
|
||||
|
||||
if create_time_from or create_time_to:
|
||||
filtered_docs = []
|
||||
for doc in docs:
|
||||
doc_create_time = doc.get("create_time", 0)
|
||||
if (create_time_from == 0 or doc_create_time >= create_time_from) and (create_time_to == 0 or doc_create_time <= create_time_to):
|
||||
filtered_docs.append(doc)
|
||||
docs = filtered_docs
|
||||
|
||||
for doc_item in docs:
|
||||
if doc_item["thumbnail"] and not doc_item["thumbnail"].startswith(IMG_BASE64_PREFIX):
|
||||
doc_item["thumbnail"] = f"/v1/document/image/{kb_id}-{doc_item['thumbnail']}"
|
||||
if doc_item.get("source_type"):
|
||||
doc_item["source_type"] = doc_item["source_type"].split("/")[0]
|
||||
if doc_item["parser_config"].get("metadata"):
|
||||
doc_item["parser_config"]["metadata"] = turn2jsonschema(doc_item["parser_config"]["metadata"])
|
||||
|
||||
return get_json_result(data={"total": tol, "docs": docs})
|
||||
except Exception as e:
|
||||
return server_error_response(e)
|
||||
|
||||
|
||||
@manager.route("/filter", methods=["POST"]) # noqa: F821
|
||||
@login_required
|
||||
async def get_filter():
|
||||
|
||||
@@ -14,23 +14,27 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
import logging
|
||||
import json
|
||||
|
||||
from quart import request
|
||||
from peewee import OperationalError
|
||||
from pydantic import ValidationError
|
||||
|
||||
from api.apps.services.document_api_service import map_doc_keys_with_run_status, validate_document_update_fields, \
|
||||
update_document_name_only, update_chunk_method_only, update_document_status_only, map_doc_keys
|
||||
from api.apps import login_required
|
||||
from api.apps.services.document_api_service import validate_document_update_fields, map_doc_keys, \
|
||||
map_doc_keys_with_run_status, update_document_name_only, update_chunk_method_only, update_document_status_only
|
||||
from api.constants import IMG_BASE64_PREFIX
|
||||
from api.db import VALID_FILE_TYPES
|
||||
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, \
|
||||
server_error_response
|
||||
from api.utils.api_utils import get_data_error_result, get_error_data_result, get_result, get_json_result, \
|
||||
server_error_response, add_tenant_id_to_kwargs, get_request_json
|
||||
from api.utils.validation_utils import (
|
||||
UpdateDocumentReq, format_validation_error_message,
|
||||
)
|
||||
from common.constants import RetCode
|
||||
from common.metadata_utils import convert_conditions, meta_filter, turn2jsonschema
|
||||
|
||||
@manager.route("/datasets/<dataset_id>/documents/<document_id>", methods=["PATCH"]) # noqa: F821
|
||||
@login_required
|
||||
@@ -316,3 +320,343 @@ async def upload_document(dataset_id, tenant_id):
|
||||
return get_result(data=renamed_doc_list)
|
||||
|
||||
|
||||
@manager.route("/datasets/<dataset_id>/documents", methods=["GET"]) # noqa: F821
|
||||
@login_required
|
||||
@add_tenant_id_to_kwargs
|
||||
def list_docs(dataset_id, tenant_id):
|
||||
"""
|
||||
List documents in a dataset.
|
||||
---
|
||||
tags:
|
||||
- Documents
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: dataset_id
|
||||
type: string
|
||||
required: true
|
||||
description: ID of the dataset.
|
||||
- in: query
|
||||
name: page
|
||||
type: integer
|
||||
required: false
|
||||
default: 1
|
||||
description: Page number.
|
||||
- in: query
|
||||
name: page_size
|
||||
type: integer
|
||||
required: false
|
||||
default: 30
|
||||
description: Number of items per page.
|
||||
- in: query
|
||||
name: orderby
|
||||
type: string
|
||||
required: false
|
||||
default: "create_time"
|
||||
description: Field to order by.
|
||||
- in: query
|
||||
name: desc
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
description: Order in descending.
|
||||
- in: query
|
||||
name: create_time_from
|
||||
type: integer
|
||||
required: false
|
||||
default: 0
|
||||
description: Unix timestamp for filtering documents created after this time. 0 means no filter.
|
||||
- in: query
|
||||
name: create_time_to
|
||||
type: integer
|
||||
required: false
|
||||
default: 0
|
||||
description: Unix timestamp for filtering documents created before this time. 0 means no filter.
|
||||
- in: query
|
||||
name: suffix
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required: false
|
||||
description: Filter by file suffix (e.g., ["pdf", "txt", "docx"]).
|
||||
- in: query
|
||||
name: run
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required: false
|
||||
description: Filter by document run status. Supports both numeric ("0", "1", "2", "3", "4") and text formats ("UNSTART", "RUNNING", "CANCEL", "DONE", "FAIL").
|
||||
- in: header
|
||||
name: Authorization
|
||||
type: string
|
||||
required: true
|
||||
description: Bearer token for authentication.
|
||||
responses:
|
||||
200:
|
||||
description: List of documents.
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of documents.
|
||||
docs:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Document ID.
|
||||
name:
|
||||
type: string
|
||||
description: Document name.
|
||||
chunk_count:
|
||||
type: integer
|
||||
description: Number of chunks.
|
||||
token_count:
|
||||
type: integer
|
||||
description: Number of tokens.
|
||||
dataset_id:
|
||||
type: string
|
||||
description: ID of the dataset.
|
||||
chunk_method:
|
||||
type: string
|
||||
description: Chunking method used.
|
||||
run:
|
||||
type: string
|
||||
description: Processing status.
|
||||
"""
|
||||
if not KnowledgebaseService.accessible(kb_id=dataset_id, user_id=tenant_id):
|
||||
logging.error(f"You don't own the dataset {dataset_id}. ")
|
||||
return get_error_data_result(message=f"You don't own the dataset {dataset_id}. ")
|
||||
|
||||
err_code, err_msg, docs, total = _get_docs_with_request(request, dataset_id)
|
||||
if err_code != RetCode.SUCCESS:
|
||||
return get_data_error_result(code=err_code, message=err_msg)
|
||||
|
||||
renamed_doc_list = [map_doc_keys(doc) for doc in docs]
|
||||
for doc_item in renamed_doc_list:
|
||||
if doc_item["thumbnail"] and not doc_item["thumbnail"].startswith(IMG_BASE64_PREFIX):
|
||||
doc_item["thumbnail"] = f"/v1/document/image/{dataset_id}-{doc_item['thumbnail']}"
|
||||
if doc_item.get("source_type"):
|
||||
doc_item["source_type"] = doc_item["source_type"].split("/")[0]
|
||||
if doc_item["parser_config"].get("metadata"):
|
||||
doc_item["parser_config"]["metadata"] = turn2jsonschema(doc_item["parser_config"]["metadata"])
|
||||
|
||||
return get_json_result(data={"total": total, "docs": renamed_doc_list})
|
||||
|
||||
|
||||
def _get_docs_with_request(req, dataset_id:str):
|
||||
"""Get documents with request parameters from a dataset.
|
||||
|
||||
This function extracts filtering parameters from the request and returns
|
||||
a list of documents matching the specified criteria.
|
||||
|
||||
Args:
|
||||
req: The request object containing query parameters.
|
||||
- page (int): Page number for pagination (default: 1).
|
||||
- page_size (int): Number of documents per page (default: 30).
|
||||
- orderby (str): Field to order by (default: "create_time").
|
||||
- desc (bool): Whether to order in descending order (default: True).
|
||||
- keywords (str): Keywords to search in document names.
|
||||
- suffix (list): File suffix filters.
|
||||
- types (list): Document type filters.
|
||||
- run (list): Processing status filters.
|
||||
- create_time_from (int): Start timestamp for time range filter.
|
||||
- create_time_to (int): End timestamp for time range filter.
|
||||
- return_empty_metadata (bool|str): Whether to return documents with empty metadata.
|
||||
- metadata_condition (str): JSON string for complex metadata conditions.
|
||||
- metadata (str): JSON string for simple metadata key-value matching.
|
||||
dataset_id: The dataset ID to retrieve documents from.
|
||||
|
||||
Returns:
|
||||
A tuple of (err_code, err_message, docs, total):
|
||||
- err_code (int): Success code (RetCode.SUCCESS) if successful, or error code if validation fails.
|
||||
- err_message (str): Empty string if successful, or error message if validation fails.
|
||||
- docs (list): List of document dictionaries matching the criteria, or empty list on error.
|
||||
- total (int): Total number of documents matching the criteria.
|
||||
|
||||
Note:
|
||||
- The function supports filtering by document types, processing status, keywords, and time range.
|
||||
- Metadata filtering supports both simple key-value matching and complex conditions with operators.
|
||||
"""
|
||||
q = req.args
|
||||
|
||||
page = int(q.get("page", 1))
|
||||
page_size = int(q.get("page_size", 30))
|
||||
|
||||
orderby = q.get("orderby", "create_time")
|
||||
desc = str(q.get("desc", "true")).strip().lower() != "false"
|
||||
keywords = q.get("keywords", "")
|
||||
|
||||
# filters - align with OpenAPI parameter names
|
||||
suffix = q.getlist("suffix")
|
||||
|
||||
types = q.getlist("types")
|
||||
if types:
|
||||
invalid_types = {t for t in types if t not in VALID_FILE_TYPES}
|
||||
if invalid_types:
|
||||
msg = f"Invalid filter conditions: {', '.join(invalid_types)} type{'s' if len(invalid_types) > 1 else ''}"
|
||||
return RetCode.DATA_ERROR, msg, [], 0
|
||||
|
||||
# map run status (text or numeric) - align with API parameter
|
||||
run_status = q.getlist("run")
|
||||
run_status_text_to_numeric = {"UNSTART": "0", "RUNNING": "1", "CANCEL": "2", "DONE": "3", "FAIL": "4"}
|
||||
run_status_converted = [run_status_text_to_numeric.get(v, v) for v in run_status]
|
||||
if run_status_converted:
|
||||
invalid_status = {s for s in run_status_converted if s not in run_status_text_to_numeric.values()}
|
||||
if invalid_status:
|
||||
msg = f"Invalid filter run status conditions: {', '.join(invalid_status)}"
|
||||
return RetCode.DATA_ERROR, msg, [], 0
|
||||
|
||||
err_code, err_message, doc_ids_filter, return_empty_metadata = _parse_doc_id_filter_with_metadata(q, dataset_id)
|
||||
if err_code != RetCode.SUCCESS:
|
||||
return err_code, err_message, [], 0
|
||||
|
||||
doc_name = q.get("name")
|
||||
doc_id = q.get("id")
|
||||
if doc_id and not DocumentService.query(id=doc_id, kb_id=dataset_id):
|
||||
return RetCode.DATA_ERROR, f"You don't own the document {doc_id}.", [], 0
|
||||
if doc_name and not DocumentService.query(name=doc_name, kb_id=dataset_id):
|
||||
return RetCode.DATA_ERROR, f"You don't own the document {doc_name}.", [], 0
|
||||
|
||||
docs, total = DocumentService.get_by_kb_id(dataset_id, page, page_size, orderby, desc, keywords, run_status_converted, types, suffix,
|
||||
doc_id=doc_id, name=doc_name, doc_ids_filter=doc_ids_filter, return_empty_metadata=return_empty_metadata)
|
||||
|
||||
# time range filter (0 means no bound)
|
||||
create_time_from = int(q.get("create_time_from", 0))
|
||||
create_time_to = int(q.get("create_time_to", 0))
|
||||
if create_time_from or create_time_to:
|
||||
docs = [d for d in docs if (create_time_from == 0 or d.get("create_time", 0) >= create_time_from) and (create_time_to == 0 or d.get("create_time", 0) <= create_time_to)]
|
||||
|
||||
return RetCode.SUCCESS, "", docs, total
|
||||
|
||||
def _parse_doc_id_filter_with_metadata(req, kb_id):
|
||||
"""Parse document ID filter based on metadata conditions from the request.
|
||||
|
||||
This function extracts and processes metadata filtering parameters from the request
|
||||
and returns a list of document IDs that match the specified criteria. It supports
|
||||
two filtering modes: simple metadata key-value matching and complex metadata
|
||||
conditions with operators.
|
||||
|
||||
Args:
|
||||
req: The request object containing filtering parameters.
|
||||
- return_empty_metadata (bool|str): If True, returns all documents regardless
|
||||
of their metadata. Can be a boolean or string "true"/"false".
|
||||
- metadata_condition (str): JSON string containing complex metadata conditions
|
||||
with optional "logic" (and/or) and "conditions" list. Each condition should
|
||||
have "name" (key), "comparison_operator", and "value" fields.
|
||||
- metadata (str): JSON string containing key-value pairs for exact metadata
|
||||
matching. Values can be a single value or list of values (OR logic within
|
||||
same key). Can include special key "empty_metadata" to indicate documents
|
||||
with empty metadata.
|
||||
kb_id: The knowledge base ID to filter documents from.
|
||||
|
||||
Returns:
|
||||
A tuple of (err_code, err_message, docs, return_empty_metadata):
|
||||
- err_code (int): Success code (RetCode.SUCCESS) if successful, or error code if validation fails.
|
||||
- err_message (str): Empty string if successful, or error message if validation fails.
|
||||
- docs (list): List of document IDs matching the metadata criteria,
|
||||
or empty list if no filter should be applied or on error.
|
||||
- return_empty_metadata (bool): The processed flag indicating whether to
|
||||
return documents with empty metadata.
|
||||
|
||||
Note:
|
||||
- When both metadata and metadata_condition are provided, they are combined with AND logic.
|
||||
- The metadata_condition uses operators like: =, !=, >, <, >=, <=, contains, not contains,
|
||||
in, not in, start with, end with, empty, not empty.
|
||||
- The metadata parameter performs exact matching where values are OR'd within the same key
|
||||
and AND'd across different keys.
|
||||
|
||||
Examples:
|
||||
Simple metadata filter (exact match):
|
||||
req = {"metadata": '{"author": ["John", "Jane"]}'}
|
||||
# Returns documents where author is John OR Jane
|
||||
|
||||
Simple metadata filter with multiple keys:
|
||||
req = {"metadata": '{"author": "John", "status": "published"}'}
|
||||
# Returns documents where author is John AND status is published
|
||||
|
||||
Complex metadata conditions:
|
||||
req = {"metadata_condition": '{"logic": "and", "conditions": [{"name": "status", "comparison_operator": "eq", "value": "published"}]}'}
|
||||
# Returns documents where status equals "published"
|
||||
|
||||
Complex conditions with multiple operators:
|
||||
req = {"metadata_condition": '{"logic": "or", "conditions": [{"name": "priority", "comparison_operator": "=", "value": "high"}, {"name": "status", "comparison_operator": "contains", "value": "urgent"}]}'}
|
||||
# Returns documents where priority is high OR status contains "urgent"
|
||||
|
||||
Return empty metadata:
|
||||
req = {"return_empty_metadata": True}
|
||||
# Returns all documents regardless of metadata
|
||||
|
||||
Combined metadata and metadata_condition:
|
||||
req = {"metadata": '{"author": "John"}', "metadata_condition": '{"logic": "and", "conditions": [{"name": "status", "comparison_operator": "=", "value": "published"}]}'}
|
||||
# Returns documents where author is John AND status equals published
|
||||
"""
|
||||
return_empty_metadata = req.get("return_empty_metadata", False)
|
||||
if isinstance(return_empty_metadata, str):
|
||||
return_empty_metadata = return_empty_metadata.lower() == "true"
|
||||
|
||||
try:
|
||||
metadata_condition = json.loads(req.get("metadata_condition", "{}"))
|
||||
except json.JSONDecodeError:
|
||||
msg = f'metadata_condition must be valid JSON: {req.get("metadata_condition")}.'
|
||||
return RetCode.DATA_ERROR, msg, [], return_empty_metadata
|
||||
try:
|
||||
metadata = json.loads(req.get("metadata", "{}"))
|
||||
except json.JSONDecodeError:
|
||||
logging.error(msg=f'metadata must be valid JSON: {req.get("metadata")}.')
|
||||
return RetCode.DATA_ERROR, "metadata must be valid JSON.", [], return_empty_metadata
|
||||
|
||||
if isinstance(metadata, dict) and metadata.get("empty_metadata"):
|
||||
return_empty_metadata = True
|
||||
metadata = {k: v for k, v in metadata.items() if k != "empty_metadata"}
|
||||
if return_empty_metadata:
|
||||
metadata_condition = {}
|
||||
metadata = {}
|
||||
else:
|
||||
if metadata_condition and not isinstance(metadata_condition, dict):
|
||||
return RetCode.DATA_ERROR, "metadata_condition must be an object.", [], return_empty_metadata
|
||||
if metadata and not isinstance(metadata, dict):
|
||||
return RetCode.DATA_ERROR, "metadata must be an object.", [], return_empty_metadata
|
||||
|
||||
doc_ids_filter = None
|
||||
metas = None
|
||||
if metadata_condition or metadata:
|
||||
metas = DocMetadataService.get_flatted_meta_by_kbs([kb_id])
|
||||
|
||||
if metadata_condition:
|
||||
doc_ids_filter = set(meta_filter(metas, convert_conditions(metadata_condition), metadata_condition.get("logic", "and")))
|
||||
if metadata_condition.get("conditions") and not doc_ids_filter:
|
||||
return RetCode.SUCCESS, "", [], return_empty_metadata
|
||||
|
||||
if metadata:
|
||||
metadata_doc_ids = None
|
||||
for key, values in metadata.items():
|
||||
if not values:
|
||||
continue
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
values = [str(v) for v in values if v is not None and str(v).strip()]
|
||||
if not values:
|
||||
continue
|
||||
key_doc_ids = set()
|
||||
for value in values:
|
||||
key_doc_ids.update(metas.get(key, {}).get(value, []))
|
||||
if metadata_doc_ids is None:
|
||||
metadata_doc_ids = key_doc_ids
|
||||
else:
|
||||
metadata_doc_ids &= key_doc_ids
|
||||
if not metadata_doc_ids:
|
||||
return RetCode.SUCCESS, "", [], return_empty_metadata
|
||||
if metadata_doc_ids is not None:
|
||||
if doc_ids_filter is None:
|
||||
doc_ids_filter = metadata_doc_ids
|
||||
else:
|
||||
doc_ids_filter &= metadata_doc_ids
|
||||
if not doc_ids_filter:
|
||||
return RetCode.SUCCESS, "", [], return_empty_metadata
|
||||
|
||||
return RetCode.SUCCESS, "", list(doc_ids_filter) if doc_ids_filter is not None else [], return_empty_metadata
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
# limitations under the License.
|
||||
#
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
from io import BytesIO
|
||||
|
||||
@@ -159,187 +158,6 @@ async def download_doc(document_id):
|
||||
)
|
||||
|
||||
|
||||
@manager.route("/datasets/<dataset_id>/documents", methods=["GET"]) # noqa: F821
|
||||
@token_required
|
||||
def list_docs(dataset_id, tenant_id):
|
||||
"""
|
||||
List documents in a dataset.
|
||||
---
|
||||
tags:
|
||||
- Documents
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: dataset_id
|
||||
type: string
|
||||
required: true
|
||||
description: ID of the dataset.
|
||||
- in: query
|
||||
name: id
|
||||
type: string
|
||||
required: false
|
||||
description: Filter by document ID.
|
||||
- in: query
|
||||
name: page
|
||||
type: integer
|
||||
required: false
|
||||
default: 1
|
||||
description: Page number.
|
||||
- in: query
|
||||
name: page_size
|
||||
type: integer
|
||||
required: false
|
||||
default: 30
|
||||
description: Number of items per page.
|
||||
- in: query
|
||||
name: orderby
|
||||
type: string
|
||||
required: false
|
||||
default: "create_time"
|
||||
description: Field to order by.
|
||||
- in: query
|
||||
name: desc
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
description: Order in descending.
|
||||
- in: query
|
||||
name: create_time_from
|
||||
type: integer
|
||||
required: false
|
||||
default: 0
|
||||
description: Unix timestamp for filtering documents created after this time. 0 means no filter.
|
||||
- in: query
|
||||
name: create_time_to
|
||||
type: integer
|
||||
required: false
|
||||
default: 0
|
||||
description: Unix timestamp for filtering documents created before this time. 0 means no filter.
|
||||
- in: query
|
||||
name: suffix
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required: false
|
||||
description: Filter by file suffix (e.g., ["pdf", "txt", "docx"]).
|
||||
- in: query
|
||||
name: run
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
required: false
|
||||
description: Filter by document run status. Supports both numeric ("0", "1", "2", "3", "4") and text formats ("UNSTART", "RUNNING", "CANCEL", "DONE", "FAIL").
|
||||
- in: header
|
||||
name: Authorization
|
||||
type: string
|
||||
required: true
|
||||
description: Bearer token for authentication.
|
||||
responses:
|
||||
200:
|
||||
description: List of documents.
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
description: Total number of documents.
|
||||
docs:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Document ID.
|
||||
name:
|
||||
type: string
|
||||
description: Document name.
|
||||
chunk_count:
|
||||
type: integer
|
||||
description: Number of chunks.
|
||||
token_count:
|
||||
type: integer
|
||||
description: Number of tokens.
|
||||
dataset_id:
|
||||
type: string
|
||||
description: ID of the dataset.
|
||||
chunk_method:
|
||||
type: string
|
||||
description: Chunking method used.
|
||||
run:
|
||||
type: string
|
||||
description: Processing status.
|
||||
"""
|
||||
if not KnowledgebaseService.accessible(kb_id=dataset_id, user_id=tenant_id):
|
||||
return get_error_data_result(message=f"You don't own the dataset {dataset_id}. ")
|
||||
|
||||
q = request.args
|
||||
document_id = q.get("id")
|
||||
name = q.get("name")
|
||||
|
||||
if document_id and not DocumentService.query(id=document_id, kb_id=dataset_id):
|
||||
return get_error_data_result(message=f"You don't own the document {document_id}.")
|
||||
if name and not DocumentService.query(name=name, kb_id=dataset_id):
|
||||
return get_error_data_result(message=f"You don't own the document {name}.")
|
||||
|
||||
page = int(q.get("page", 1))
|
||||
page_size = int(q.get("page_size", 30))
|
||||
orderby = q.get("orderby", "create_time")
|
||||
desc = str(q.get("desc", "true")).strip().lower() != "false"
|
||||
keywords = q.get("keywords", "")
|
||||
|
||||
# filters - align with OpenAPI parameter names
|
||||
suffix = q.getlist("suffix")
|
||||
run_status = q.getlist("run")
|
||||
create_time_from = int(q.get("create_time_from", 0))
|
||||
create_time_to = int(q.get("create_time_to", 0))
|
||||
metadata_condition_raw = q.get("metadata_condition")
|
||||
metadata_condition = {}
|
||||
if metadata_condition_raw:
|
||||
try:
|
||||
metadata_condition = json.loads(metadata_condition_raw)
|
||||
except Exception:
|
||||
return get_error_data_result(message="metadata_condition must be valid JSON.")
|
||||
if metadata_condition and not isinstance(metadata_condition, dict):
|
||||
return get_error_data_result(message="metadata_condition must be an object.")
|
||||
|
||||
# map run status (text or numeric) - align with API parameter
|
||||
run_status_text_to_numeric = {"UNSTART": "0", "RUNNING": "1", "CANCEL": "2", "DONE": "3", "FAIL": "4"}
|
||||
run_status_converted = [run_status_text_to_numeric.get(v, v) for v in run_status]
|
||||
|
||||
doc_ids_filter = None
|
||||
if metadata_condition:
|
||||
metas = DocMetadataService.get_flatted_meta_by_kbs([dataset_id])
|
||||
doc_ids_filter = meta_filter(metas, convert_conditions(metadata_condition), metadata_condition.get("logic", "and"))
|
||||
if metadata_condition.get("conditions") and not doc_ids_filter:
|
||||
return get_result(data={"total": 0, "docs": []})
|
||||
|
||||
docs, total = DocumentService.get_list(dataset_id, page, page_size, orderby, desc, keywords, document_id, name, suffix, run_status_converted, doc_ids_filter)
|
||||
|
||||
# time range filter (0 means no bound)
|
||||
if create_time_from or create_time_to:
|
||||
docs = [d for d in docs if (create_time_from == 0 or d.get("create_time", 0) >= create_time_from) and (create_time_to == 0 or d.get("create_time", 0) <= create_time_to)]
|
||||
|
||||
# rename keys + map run status back to text for output
|
||||
key_mapping = {
|
||||
"chunk_num": "chunk_count",
|
||||
"kb_id": "dataset_id",
|
||||
"token_num": "token_count",
|
||||
"parser_id": "chunk_method",
|
||||
}
|
||||
run_status_numeric_to_text = {"0": "UNSTART", "1": "RUNNING", "2": "CANCEL", "3": "DONE", "4": "FAIL"}
|
||||
|
||||
output_docs = []
|
||||
for d in docs:
|
||||
renamed_doc = {key_mapping.get(k, k): v for k, v in d.items()}
|
||||
if "run" in d:
|
||||
renamed_doc["run"] = run_status_numeric_to_text.get(str(d["run"]), d["run"])
|
||||
output_docs.append(renamed_doc)
|
||||
|
||||
return get_result(data={"total": total, "docs": output_docs})
|
||||
|
||||
|
||||
@manager.route("/datasets/<dataset_id>/metadata/update", methods=["POST"]) # noqa: F821
|
||||
@token_required
|
||||
async def metadata_batch_update(dataset_id, tenant_id):
|
||||
|
||||
@@ -165,6 +165,7 @@ def validate_document_update_fields(update_doc_req:UpdateDocumentReq, doc, req):
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def map_doc_keys(doc):
|
||||
"""
|
||||
Rename document keys to match API response format.
|
||||
@@ -241,9 +242,9 @@ def _process_run_mapping(doc, run_status):
|
||||
|
||||
Args:
|
||||
doc: The document model from the database OR a dictionary.
|
||||
run_status: Optional explicit run status value. If not provided:
|
||||
- If doc has 'run' field, it will be mapped using run_mapping
|
||||
- Otherwise, 'run' will be set to 'UNSTART' (for new uploads)
|
||||
run_status: Optional explicit run status value.
|
||||
If provided, 'run' field of doc will be set to run_status.
|
||||
If not provided, 'run' will be set to 'UNSTART' (for new uploads)
|
||||
|
||||
Returns:
|
||||
A dictionary with renamed keys for API response.
|
||||
@@ -262,5 +263,3 @@ def _process_run_mapping(doc, run_status):
|
||||
|
||||
doc["run"] = run_mapping[run_status]
|
||||
return doc
|
||||
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ class DocumentService(CommonService):
|
||||
|
||||
@classmethod
|
||||
@DB.connection_context()
|
||||
def get_by_kb_id(cls, kb_id, page_number, items_per_page, orderby, desc, keywords, run_status, types, suffix, doc_ids=None, return_empty_metadata=False):
|
||||
def get_by_kb_id(cls, kb_id, page_number, items_per_page, orderby, desc, keywords, run_status, types, suffix, doc_id=None, name=None, doc_ids_filter=None, return_empty_metadata=False):
|
||||
fields = cls.get_cls_model_fields()
|
||||
if keywords:
|
||||
docs = (
|
||||
@@ -147,17 +147,19 @@ class DocumentService(CommonService):
|
||||
.join(User, on=(cls.model.created_by == User.id), join_type=JOIN.LEFT_OUTER)
|
||||
.where(cls.model.kb_id == kb_id)
|
||||
)
|
||||
|
||||
if doc_ids:
|
||||
docs = docs.where(cls.model.id.in_(doc_ids))
|
||||
if doc_id:
|
||||
docs = docs.where(cls.model.id == doc_id)
|
||||
if doc_ids_filter:
|
||||
docs = docs.where(cls.model.id.in_(doc_ids_filter))
|
||||
if run_status:
|
||||
docs = docs.where(cls.model.run.in_(run_status))
|
||||
if types:
|
||||
docs = docs.where(cls.model.type.in_(types))
|
||||
if suffix:
|
||||
docs = docs.where(cls.model.suffix.in_(suffix))
|
||||
if name:
|
||||
docs = docs.where(cls.model.name == name)
|
||||
|
||||
metadata_map = {}
|
||||
if return_empty_metadata:
|
||||
metadata_map = DocMetadataService.get_metadata_for_documents(None, kb_id)
|
||||
doc_ids_with_metadata = set(metadata_map.keys())
|
||||
|
||||
Reference in New Issue
Block a user