by-qa 0.4.4.dev3__tar.gz → 0.4.4.dev5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/PKG-INFO +1 -1
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/pyproject.toml +1 -1
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/api/routes.py +223 -135
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/api/schemas.py +31 -2
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/infrastructure/storage.py +4 -2
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/infrastructure/storage_s3.py +51 -13
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/knowledge_fs_entry_repository.py +22 -0
- by_qa-0.4.4.dev5/src/by_qa/knowledge_base/services/file_upload_source.py +42 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/knowledge_base_service.py +35 -1
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/knowledge_item_ingestion_service.py +31 -12
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/zip_batch_import_service.py +90 -72
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/uv.lock +1 -1
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/.env.example +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/.github/workflows/ci.yml +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/.github/workflows/release.yml +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/.gitignore +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/.pre-commit-config.yaml +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/AGENTS.md +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/CLAUDE.md +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/CONTEXT.md +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/Dockerfile +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/LICENSE +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/Makefile +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/README.md +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/README_zh.md +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/docker/minio/init/init-minio.sh +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/docker/opengauss/custom/Dockerfile +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/docker/opengauss/init/init-opengauss.sh +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/docker-compose.kb-stack.yml +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/eval/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/eval/cli.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/eval/judge.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/eval/models.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/eval/runner.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/pylintrc +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/pytest.ini +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/config.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/core/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/core/exceptions.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/core/framework_client.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/core/logger.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/core/model_config.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/api/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/api/knowledge_entity_schemas.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/api/metadata_schemas.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/build_status.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/dsl/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/dsl/compiler.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/dsl/errors.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/dsl/validator.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/events.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/infrastructure/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/infrastructure/database.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/infrastructure/object_storage.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/infrastructure/runtime.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/metadata_types.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/file_metadata_value_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/knowledge_base_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/knowledge_build_acceptance_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/knowledge_build_batch_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/knowledge_build_task_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/knowledge_entity_asset_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/knowledge_entity_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/knowledge_fetch_cache_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/knowledge_file_reference_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/knowledge_file_update_timeline_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/knowledge_item_chunk_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/knowledge_item_search_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/knowledge_semantic_processing_batch_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/knowledge_semantic_processing_task_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/metadata_search_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/processing_task_query_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/repositories/retrieval_projection_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/bootstrap_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/cache_file_lock.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/document_update_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/embedding_query_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/entry_metadata.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/errors.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/file_build_background_runner.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/file_build_execution_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/file_build_models.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/file_build_mutation_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/file_build_processing_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/file_build_terminal_event_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/file_metadata_query_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/file_metadata_update_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/knowledge_document_metadata.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/knowledge_entity_background_runner.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/knowledge_entity_discovery.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/knowledge_entity_enrichment.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/knowledge_entity_intelligence.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/knowledge_entity_processing_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/knowledge_entity_synonym_resolution.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/knowledge_entity_task_worker.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/knowledge_fetch_cache_cleanup_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/knowledge_item_search_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/markdown_front_matter.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/markdown_reference_resolver.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/markdown_reference_rewriter.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/markdown_update_summary_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/metadata_search_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/services/semantic_task_mutation_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/000_extensions.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/001_knowledge_base.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/002_knowledge_fs_entry.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/003_knowledge_chunk.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/004_retrieval_projection.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/005_knowledge_fetch_cache_index.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/006_knowledge_build_task.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/009_knowledge_fs_entry_indexes.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/010_knowledge_chunk_indexes.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/011_retrieval_projection_indexes.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/012_knowledge_fetch_cache_index_indexes.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/013_knowledge_build_task_indexes.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/014_embedding_table.sql.tpl +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/015_metadata_property_def.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/016_metadata_property_def_indexes.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/017_file_metadata_value.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/018_file_metadata_value_indexes.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/019_knowledge_fs_entry_virtual_path.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/020_knowledge_fs_entry_virtual_path_indexes.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/021_knowledge_fs_entry_virtual_path_backfill.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/022_knowledge_fs_entry_top_level_sibling_unique.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/023_metadata_property_def_name_unique.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/024_knowledge_base_name_unique.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/025_metadata_value_self_contained_migration.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/026_knowledge_file_reference.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/027_knowledge_file_update_timeline.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/028_knowledge_fs_entry_checksum_index.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/029_knowledge_semantic_processing_task.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/030_knowledge_file_reference_semantic_extension.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/031_knowledge_file_document_kind_backfill.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/032_knowledge_schema_migration.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/033_knowledge_entity.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/034_knowledge_entity_embedding_table.sql.tpl +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/035_knowledge_entity_discovery_schema.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/036_knowledge_entity_embedding_full_only.sql.tpl +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/037_knowledge_semantic_processing_directory_scope.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/038_knowledge_build_batch.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/039_knowledge_build_task_background_extension.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/040_knowledge_build_task_legacy_backfill.sql.tpl +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/041_knowledge_build_task_active_constraints.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_base/sql/042_knowledge_fs_entry_upload_uniqueness.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_build/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_build/runtime.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_build/services/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_build/services/document_chunking_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_build/services/heading_patterns.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_common/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_common/exceptions.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_common/kb_path_utils.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_common/markdown_reference.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_common/schemas.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/knowledge_common/text_segmentation.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/main.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/agents/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/agents/answer_synthesizer.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/agents/multi_hop_react.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/agents/multi_hop_summarizer.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/agents/query_decomposer.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/agents/recursion_fallback.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/agents/single_hop_react.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/agents/standalone_question_rewriter.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/agents/subanswer_aggregator.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/common/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/common/base_engine.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/common/config.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/common/context.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/common/context_manager.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/common/event_filter.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/common/exceptions.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/common/fallback_messages.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/common/messages.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/common/middleware/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/common/middleware/tool_call_guard.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/common/models.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/common/operation_registry.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/common/prompt_fragments.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/common/reducers.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/common/state.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/engines/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/engines/fast/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/engines/fast/engine.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/engines/fast/graph.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/engines/fast/nodes/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/engines/fast/nodes/retrieve.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/engines/fast/state.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/engines/fast/types.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/engines/instant/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/engines/instant/engine.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/engines/instant/graph.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/engines/instant/nodes/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/engines/instant/nodes/final_answer.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/engines/instant/nodes/router.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/engines/instant/nodes/state_cleanup.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/engines/instant/state.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/engines/instant/types.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/services/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/services/checkpointer_factory.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/services/llm_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/services/opengauss_checkpointer.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/services/session_history.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/tools/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/tools/dsl_guide.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/tools/knowledge_tools.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/tools/operations/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/tools/operations/base.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev5}/src/by_qa/qa/tools/operations/knowledge_search.py +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"""Route registration for knowledge base APIs."""
|
|
2
2
|
|
|
3
|
-
import io
|
|
4
3
|
import json
|
|
5
4
|
import mimetypes
|
|
6
5
|
import zipfile
|
|
6
|
+
from contextlib import AsyncExitStack
|
|
7
7
|
from inspect import isawaitable
|
|
8
|
-
from pathlib import PurePosixPath
|
|
8
|
+
from pathlib import Path, PurePosixPath
|
|
9
|
+
from tempfile import TemporaryDirectory
|
|
9
10
|
from typing import Any, Callable, Optional
|
|
10
11
|
from urllib.parse import quote
|
|
11
12
|
|
|
@@ -43,6 +44,7 @@ from by_qa.knowledge_base.api.schemas import (
|
|
|
43
44
|
FileBuildStatusRequest,
|
|
44
45
|
FileToMarkdownIndexRequest,
|
|
45
46
|
KnowledgeBaseFileCountRequest,
|
|
47
|
+
KnowledgeBaseStatusRequest,
|
|
46
48
|
KnowledgeItemDownloadRequest,
|
|
47
49
|
KnowledgeItemGlobRequest,
|
|
48
50
|
KnowledgeItemListDirRequest,
|
|
@@ -64,6 +66,11 @@ from by_qa.knowledge_base.services.errors import (
|
|
|
64
66
|
KnowledgeBaseConfigurationError,
|
|
65
67
|
KnowledgeBaseValidationError,
|
|
66
68
|
)
|
|
69
|
+
from by_qa.knowledge_base.services.file_upload_source import (
|
|
70
|
+
UPLOAD_CHUNK_SIZE,
|
|
71
|
+
FileUploadRequest,
|
|
72
|
+
file_io,
|
|
73
|
+
)
|
|
67
74
|
from by_qa.knowledge_base.services.knowledge_item_ingestion_service import (
|
|
68
75
|
convert_uploaded_file_to_markdown,
|
|
69
76
|
)
|
|
@@ -506,6 +513,11 @@ def register_routes(
|
|
|
506
513
|
|
|
507
514
|
@app.post(
|
|
508
515
|
"/api/v1/knowledgeBases/fileCount",
|
|
516
|
+
deprecated=True,
|
|
517
|
+
description=(
|
|
518
|
+
"Deprecated. Use POST /api/v1/knowledgeBases/status with knCodeList "
|
|
519
|
+
"to retrieve fileCount and spaceSize for each knowledge base."
|
|
520
|
+
),
|
|
509
521
|
openapi_extra={
|
|
510
522
|
"requestBody": {
|
|
511
523
|
"required": True,
|
|
@@ -559,6 +571,62 @@ def register_routes(
|
|
|
559
571
|
)
|
|
560
572
|
return _documented_success_response(result_object=result)
|
|
561
573
|
|
|
574
|
+
@app.post(
|
|
575
|
+
"/api/v1/knowledgeBases/status",
|
|
576
|
+
openapi_extra={
|
|
577
|
+
"requestBody": {
|
|
578
|
+
"required": True,
|
|
579
|
+
"content": {
|
|
580
|
+
"application/json": {
|
|
581
|
+
"schema": KnowledgeBaseStatusRequest.model_json_schema(
|
|
582
|
+
by_alias=True
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
)
|
|
589
|
+
async def knowledge_base_status(http_request: Request):
|
|
590
|
+
# Parse locally so malformed bodies also honor this endpoint's HTTP 422
|
|
591
|
+
# contract rather than the application's legacy HTTP 200 error handler.
|
|
592
|
+
try:
|
|
593
|
+
request = KnowledgeBaseStatusRequest.model_validate(
|
|
594
|
+
await http_request.json()
|
|
595
|
+
)
|
|
596
|
+
except ValueError:
|
|
597
|
+
return _documented_error_response(
|
|
598
|
+
result_msg="request validation failed",
|
|
599
|
+
status_code=422,
|
|
600
|
+
preserve_http_status=True,
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
logger.info(
|
|
604
|
+
"knowledge_base_status request received: kb_code_list=%s",
|
|
605
|
+
request.kb_code_list,
|
|
606
|
+
)
|
|
607
|
+
try:
|
|
608
|
+
service = await get_knowledge_base_service()
|
|
609
|
+
result = await service.status(request)
|
|
610
|
+
except KnowledgeBaseConfigurationError as exc:
|
|
611
|
+
return _documented_error_response(
|
|
612
|
+
result_msg=str(exc), status_code=503, preserve_http_status=True
|
|
613
|
+
)
|
|
614
|
+
except KnowledgeBaseValidationError as exc:
|
|
615
|
+
return _documented_error_response(
|
|
616
|
+
result_msg=str(exc), status_code=422, preserve_http_status=True
|
|
617
|
+
)
|
|
618
|
+
except Exception as exc:
|
|
619
|
+
logger.exception(
|
|
620
|
+
"knowledge_base_status unexpected error: kb_code_list=%s",
|
|
621
|
+
request.kb_code_list,
|
|
622
|
+
)
|
|
623
|
+
return _documented_error_response(
|
|
624
|
+
result_msg=str(exc) or "internal error",
|
|
625
|
+
status_code=500,
|
|
626
|
+
preserve_http_status=True,
|
|
627
|
+
)
|
|
628
|
+
return _documented_success_response(result_object=result)
|
|
629
|
+
|
|
562
630
|
@app.post("/api/v1/directories/create")
|
|
563
631
|
async def create_directory(
|
|
564
632
|
background_tasks: BackgroundTasks,
|
|
@@ -756,148 +824,168 @@ def register_routes(
|
|
|
756
824
|
skip_if_duplicate: bool = Form(False, alias="skipIfDuplicate"),
|
|
757
825
|
metadata: str | None = Form(None, alias="metadata"),
|
|
758
826
|
):
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
"
|
|
767
|
-
|
|
768
|
-
|
|
827
|
+
async with AsyncExitStack() as stack:
|
|
828
|
+
try:
|
|
829
|
+
filename = (
|
|
830
|
+
(file_content.filename or "") if file_content is not None else ""
|
|
831
|
+
)
|
|
832
|
+
is_zip = extract_zip and filename.lower().endswith(".zip")
|
|
833
|
+
if is_zip and file_content is not None:
|
|
834
|
+
temp_dir = TemporaryDirectory(prefix="byqa-upload-")
|
|
835
|
+
stack.push_async_callback(file_io, temp_dir.cleanup)
|
|
836
|
+
payload = Path(temp_dir.name) / "upload.zip"
|
|
837
|
+
with payload.open("wb") as stream:
|
|
838
|
+
while chunk := await file_content.read(UPLOAD_CHUNK_SIZE):
|
|
839
|
+
await file_io(stream.write, chunk)
|
|
840
|
+
request_model = FileUploadRequest
|
|
841
|
+
else:
|
|
842
|
+
payload = (
|
|
843
|
+
await file_content.read() if file_content is not None else None
|
|
844
|
+
)
|
|
845
|
+
request_model = KnowledgeItemUploadRequest
|
|
846
|
+
request = request_model.model_validate(
|
|
847
|
+
{
|
|
848
|
+
"knCode": kn_code,
|
|
849
|
+
"filePath": file_path,
|
|
850
|
+
"fileDescription": file_description,
|
|
851
|
+
"fileContent": payload,
|
|
852
|
+
"mimeType": (
|
|
853
|
+
file_content.content_type
|
|
854
|
+
if file_content is not None
|
|
855
|
+
else None
|
|
856
|
+
),
|
|
857
|
+
"processFrontMatter": process_front_matter,
|
|
858
|
+
"extractZip": extract_zip,
|
|
859
|
+
"skipIfDuplicate": skip_if_duplicate,
|
|
860
|
+
"metadata": _parse_optional_metadata_form(metadata),
|
|
861
|
+
}
|
|
862
|
+
)
|
|
863
|
+
except (ValidationError, ValueError) as exc:
|
|
864
|
+
return _documented_error_response(
|
|
865
|
+
result_msg="request validation failed",
|
|
866
|
+
result_object=(
|
|
867
|
+
{"errors": json.loads(exc.json())}
|
|
868
|
+
if isinstance(exc, ValidationError)
|
|
869
|
+
else {"errors": [{"msg": str(exc)}]}
|
|
769
870
|
),
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
result_object=(
|
|
780
|
-
{"errors": json.loads(exc.json())}
|
|
781
|
-
if isinstance(exc, ValidationError)
|
|
782
|
-
else {"errors": [{"msg": str(exc)}]}
|
|
783
|
-
),
|
|
784
|
-
status_code=422,
|
|
871
|
+
status_code=422,
|
|
872
|
+
)
|
|
873
|
+
logger.info(
|
|
874
|
+
"upload_file request received: kb_code=%s, file_path=%s, has_description=%s, process_front_matter=%s, extract_zip=%s",
|
|
875
|
+
request.kb_code,
|
|
876
|
+
request.file_path,
|
|
877
|
+
request.file_description is not None,
|
|
878
|
+
request.process_front_matter,
|
|
879
|
+
request.extract_zip,
|
|
785
880
|
)
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
881
|
+
try:
|
|
882
|
+
service = await get_knowledge_item_ingestion_service()
|
|
883
|
+
if request.extract_zip and filename.lower().endswith(".zip"):
|
|
884
|
+
if not await file_io(zipfile.is_zipfile, payload):
|
|
885
|
+
return _documented_error_response(
|
|
886
|
+
result_msg="invalid zip file",
|
|
887
|
+
result_object={},
|
|
888
|
+
status_code=422,
|
|
889
|
+
)
|
|
890
|
+
directory_service = await get_knowledge_base_service()
|
|
891
|
+
batch_service = ZipBatchImportService(
|
|
892
|
+
ingestion_service=service,
|
|
893
|
+
directory_service=directory_service,
|
|
894
|
+
)
|
|
895
|
+
result = await batch_service.import_zip(
|
|
896
|
+
kb_code=request.kb_code,
|
|
897
|
+
target_dir=request.file_path,
|
|
898
|
+
zip_path=payload,
|
|
899
|
+
process_front_matter=request.process_front_matter,
|
|
900
|
+
file_description=request.file_description,
|
|
901
|
+
skip_if_duplicate=request.skip_if_duplicate,
|
|
902
|
+
metadata=request.metadata,
|
|
903
|
+
)
|
|
904
|
+
result_object = {
|
|
905
|
+
"data": [
|
|
906
|
+
item.model_dump(by_alias=True) for item in result.data
|
|
907
|
+
],
|
|
908
|
+
"summary": result.summary.model_dump(by_alias=True),
|
|
909
|
+
}
|
|
910
|
+
if result.post_process_errors:
|
|
911
|
+
result_object["postProcessErrors"] = result.post_process_errors
|
|
912
|
+
if result.summary.succeeded > 0:
|
|
913
|
+
await _try_schedule_event(
|
|
914
|
+
background_tasks,
|
|
915
|
+
lambda: build_resource_event(
|
|
916
|
+
event_type=ResourceEventType.FILE_IMPORTED,
|
|
917
|
+
kb_code=request.kb_code,
|
|
918
|
+
source_path=None,
|
|
919
|
+
target_path=_ensure_leading_slash(request.file_path),
|
|
920
|
+
items=[
|
|
921
|
+
{
|
|
922
|
+
"sourcePath": item.file_path,
|
|
923
|
+
"targetPath": (
|
|
924
|
+
item.file_path if item.success else None
|
|
925
|
+
),
|
|
926
|
+
"resourceType": item.resource_type,
|
|
927
|
+
"success": item.success,
|
|
928
|
+
"error": item.error,
|
|
929
|
+
}
|
|
930
|
+
for item in result.data
|
|
931
|
+
],
|
|
932
|
+
result=result.summary.model_dump(),
|
|
933
|
+
),
|
|
934
|
+
)
|
|
935
|
+
return _documented_success_response(result_object=result_object)
|
|
936
|
+
# single file
|
|
937
|
+
file_path_norm = "/" + request.file_path.strip("/")
|
|
938
|
+
segments = [s for s in file_path_norm.split("/") if s]
|
|
939
|
+
if any(s == ".." for s in segments):
|
|
799
940
|
return _documented_error_response(
|
|
800
|
-
result_msg="
|
|
801
|
-
result_object={},
|
|
802
|
-
status_code=422,
|
|
941
|
+
result_msg="unsafe path", result_object={}, status_code=422
|
|
803
942
|
)
|
|
804
|
-
|
|
805
|
-
batch_service = ZipBatchImportService(
|
|
806
|
-
ingestion_service=service,
|
|
807
|
-
directory_service=directory_service,
|
|
808
|
-
)
|
|
809
|
-
result = await batch_service.import_zip(
|
|
810
|
-
kb_code=request.kb_code,
|
|
811
|
-
target_dir=request.file_path,
|
|
812
|
-
zip_bytes=payload,
|
|
813
|
-
process_front_matter=request.process_front_matter,
|
|
814
|
-
file_description=request.file_description,
|
|
815
|
-
skip_if_duplicate=request.skip_if_duplicate,
|
|
816
|
-
metadata=request.metadata,
|
|
817
|
-
)
|
|
943
|
+
await service.upload_file(request)
|
|
818
944
|
result_object = {
|
|
819
|
-
"data": [
|
|
820
|
-
|
|
945
|
+
"data": [
|
|
946
|
+
{
|
|
947
|
+
"filePath": file_path_norm,
|
|
948
|
+
"success": True,
|
|
949
|
+
"error": None,
|
|
950
|
+
}
|
|
951
|
+
],
|
|
952
|
+
"summary": {"total": 1, "succeeded": 1, "failed": 0},
|
|
821
953
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
items=[
|
|
833
|
-
{
|
|
834
|
-
"sourcePath": item.file_path,
|
|
835
|
-
"targetPath": (
|
|
836
|
-
item.file_path if item.success else None
|
|
837
|
-
),
|
|
838
|
-
"resourceType": item.resource_type,
|
|
839
|
-
"success": item.success,
|
|
840
|
-
"error": item.error,
|
|
841
|
-
}
|
|
842
|
-
for item in result.data
|
|
843
|
-
],
|
|
844
|
-
result=result.summary.model_dump(),
|
|
845
|
-
),
|
|
846
|
-
)
|
|
954
|
+
await _try_schedule_event(
|
|
955
|
+
background_tasks,
|
|
956
|
+
lambda: build_resource_event(
|
|
957
|
+
event_type=ResourceEventType.FILE_IMPORTED,
|
|
958
|
+
kb_code=request.kb_code,
|
|
959
|
+
source_path=None,
|
|
960
|
+
target_path=file_path_norm,
|
|
961
|
+
result=result_object["summary"],
|
|
962
|
+
),
|
|
963
|
+
)
|
|
847
964
|
return _documented_success_response(result_object=result_object)
|
|
848
|
-
|
|
849
|
-
file_path_norm = "/" + request.file_path.strip("/")
|
|
850
|
-
segments = [s for s in file_path_norm.split("/") if s]
|
|
851
|
-
if any(s == ".." for s in segments):
|
|
965
|
+
except KnowledgeBaseConfigurationError as exc:
|
|
852
966
|
return _documented_error_response(
|
|
853
|
-
result_msg=
|
|
967
|
+
result_msg=str(exc),
|
|
968
|
+
result_object={},
|
|
969
|
+
status_code=503,
|
|
970
|
+
)
|
|
971
|
+
except KnowledgeBaseValidationError as exc:
|
|
972
|
+
return _documented_error_response(
|
|
973
|
+
result_msg=str(exc),
|
|
974
|
+
result_object={},
|
|
975
|
+
status_code=422,
|
|
976
|
+
)
|
|
977
|
+
except Exception as exc:
|
|
978
|
+
logger.exception(
|
|
979
|
+
"upload_file unexpected error: kb_code=%s, file_path=%s, error=%s",
|
|
980
|
+
request.kb_code,
|
|
981
|
+
request.file_path,
|
|
982
|
+
exc,
|
|
983
|
+
)
|
|
984
|
+
return _documented_error_response(
|
|
985
|
+
result_msg=str(exc) or "internal error",
|
|
986
|
+
result_object={},
|
|
987
|
+
status_code=500,
|
|
854
988
|
)
|
|
855
|
-
await service.upload_file(request)
|
|
856
|
-
result_object = {
|
|
857
|
-
"data": [
|
|
858
|
-
{
|
|
859
|
-
"filePath": file_path_norm,
|
|
860
|
-
"success": True,
|
|
861
|
-
"error": None,
|
|
862
|
-
}
|
|
863
|
-
],
|
|
864
|
-
"summary": {"total": 1, "succeeded": 1, "failed": 0},
|
|
865
|
-
}
|
|
866
|
-
await _try_schedule_event(
|
|
867
|
-
background_tasks,
|
|
868
|
-
lambda: build_resource_event(
|
|
869
|
-
event_type=ResourceEventType.FILE_IMPORTED,
|
|
870
|
-
kb_code=request.kb_code,
|
|
871
|
-
source_path=None,
|
|
872
|
-
target_path=file_path_norm,
|
|
873
|
-
result=result_object["summary"],
|
|
874
|
-
),
|
|
875
|
-
)
|
|
876
|
-
return _documented_success_response(result_object=result_object)
|
|
877
|
-
except KnowledgeBaseConfigurationError as exc:
|
|
878
|
-
return _documented_error_response(
|
|
879
|
-
result_msg=str(exc),
|
|
880
|
-
result_object={},
|
|
881
|
-
status_code=503,
|
|
882
|
-
)
|
|
883
|
-
except KnowledgeBaseValidationError as exc:
|
|
884
|
-
return _documented_error_response(
|
|
885
|
-
result_msg=str(exc),
|
|
886
|
-
result_object={},
|
|
887
|
-
status_code=422,
|
|
888
|
-
)
|
|
889
|
-
except Exception as exc:
|
|
890
|
-
logger.exception(
|
|
891
|
-
"upload_file unexpected error: kb_code=%s, file_path=%s, error=%s",
|
|
892
|
-
request.kb_code,
|
|
893
|
-
request.file_path,
|
|
894
|
-
exc,
|
|
895
|
-
)
|
|
896
|
-
return _documented_error_response(
|
|
897
|
-
result_msg=str(exc) or "internal error",
|
|
898
|
-
result_object={},
|
|
899
|
-
status_code=500,
|
|
900
|
-
)
|
|
901
989
|
|
|
902
990
|
@app.post("/api/v1/knowledgeItems/update")
|
|
903
991
|
@app.post("/api/v1/knowledge-items/update")
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
"""Pydantic schemas for knowledge base APIs."""
|
|
2
2
|
|
|
3
|
-
from typing import Any, Literal, Optional
|
|
3
|
+
from typing import Annotated, Any, Literal, Optional
|
|
4
4
|
|
|
5
|
-
from pydantic import
|
|
5
|
+
from pydantic import (
|
|
6
|
+
AliasChoices,
|
|
7
|
+
BaseModel,
|
|
8
|
+
ConfigDict,
|
|
9
|
+
Field,
|
|
10
|
+
StringConstraints,
|
|
11
|
+
field_validator,
|
|
12
|
+
model_validator,
|
|
13
|
+
)
|
|
6
14
|
|
|
7
15
|
|
|
8
16
|
def _normalize_api_path(path: str, *, allow_root: bool) -> str:
|
|
@@ -65,6 +73,27 @@ class KnowledgeBaseFileCountRequest(BaseModel):
|
|
|
65
73
|
)
|
|
66
74
|
|
|
67
75
|
|
|
76
|
+
class KnowledgeBaseStatusRequest(BaseModel):
|
|
77
|
+
"""Request body for batch file counts and logical storage sizes."""
|
|
78
|
+
|
|
79
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
80
|
+
|
|
81
|
+
kb_code_list: list[
|
|
82
|
+
Annotated[
|
|
83
|
+
str, StringConstraints(strict=True, strip_whitespace=True, min_length=1)
|
|
84
|
+
]
|
|
85
|
+
] = Field(
|
|
86
|
+
min_length=1,
|
|
87
|
+
strict=True,
|
|
88
|
+
validation_alias=AliasChoices("knCodeList", "kb_code_list"),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@field_validator("kb_code_list")
|
|
92
|
+
@classmethod
|
|
93
|
+
def deduplicate_codes(cls, codes: list[str]) -> list[str]:
|
|
94
|
+
return list(dict.fromkeys(codes))
|
|
95
|
+
|
|
96
|
+
|
|
68
97
|
class UpdateKnowledgeBaseRequest(BaseModel):
|
|
69
98
|
"""Request body for updating knowledge base business fields."""
|
|
70
99
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
from typing import Protocol, runtime_checkable
|
|
7
8
|
|
|
8
9
|
|
|
@@ -73,10 +74,11 @@ class KnowledgeStorageProvider(Protocol):
|
|
|
73
74
|
async def write(
|
|
74
75
|
self,
|
|
75
76
|
location: StorageLocation,
|
|
76
|
-
content: bytes,
|
|
77
|
+
content: bytes | Path,
|
|
77
78
|
*,
|
|
78
79
|
content_type: str,
|
|
79
|
-
) -> StoredObject:
|
|
80
|
+
) -> StoredObject:
|
|
81
|
+
"""Write bytes or stream a local Path; the caller owns the source file."""
|
|
80
82
|
|
|
81
83
|
async def read(self, location: StorageLocation) -> bytes: ...
|
|
82
84
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from pathlib import PurePosixPath
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
9
|
from botocore.exceptions import ClientError
|
|
@@ -20,6 +20,7 @@ from by_qa.knowledge_base.infrastructure.storage import (
|
|
|
20
20
|
StorageOperationError,
|
|
21
21
|
StoredObject,
|
|
22
22
|
)
|
|
23
|
+
from by_qa.knowledge_base.services.file_upload_source import file_io
|
|
23
24
|
|
|
24
25
|
_NOT_FOUND_CODES = {"NoSuchKey", "404", "NotFound"}
|
|
25
26
|
_MISSING_BUCKET_CODES = {"NoSuchBucket"}
|
|
@@ -71,25 +72,62 @@ class S3KnowledgeStorageProvider:
|
|
|
71
72
|
async def write(
|
|
72
73
|
self,
|
|
73
74
|
location: StorageLocation,
|
|
74
|
-
content: bytes,
|
|
75
|
+
content: bytes | Path,
|
|
75
76
|
*,
|
|
76
77
|
content_type: str,
|
|
77
78
|
) -> StoredObject:
|
|
79
|
+
size = content.stat().st_size if isinstance(content, Path) else len(content)
|
|
80
|
+
target = {"Bucket": location.namespace, "Key": location.key}
|
|
78
81
|
try:
|
|
79
82
|
async with self.storage._client() as s3: # noqa: SLF001
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
if isinstance(content, bytes):
|
|
84
|
+
await s3.put_object(
|
|
85
|
+
**target, Body=content, ContentType=content_type
|
|
86
|
+
)
|
|
87
|
+
else:
|
|
88
|
+
# One part in flight per file; ZIP workers provide concurrency.
|
|
89
|
+
part_size = max(8 * 1024 * 1024, (size + 9999) // 10000)
|
|
90
|
+
with content.open("rb") as stream:
|
|
91
|
+
chunk = await file_io(stream.read, part_size)
|
|
92
|
+
if size <= part_size:
|
|
93
|
+
await s3.put_object(
|
|
94
|
+
**target, Body=chunk, ContentType=content_type
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
response = await s3.create_multipart_upload(
|
|
98
|
+
**target, ContentType=content_type
|
|
99
|
+
)
|
|
100
|
+
upload_id = response["UploadId"]
|
|
101
|
+
parts = []
|
|
102
|
+
try:
|
|
103
|
+
while chunk:
|
|
104
|
+
number = len(parts) + 1
|
|
105
|
+
response = await s3.upload_part(
|
|
106
|
+
**target,
|
|
107
|
+
UploadId=upload_id,
|
|
108
|
+
PartNumber=number,
|
|
109
|
+
Body=chunk,
|
|
110
|
+
)
|
|
111
|
+
parts.append(
|
|
112
|
+
{"ETag": response["ETag"], "PartNumber": number}
|
|
113
|
+
)
|
|
114
|
+
chunk = await file_io(stream.read, part_size)
|
|
115
|
+
await s3.complete_multipart_upload(
|
|
116
|
+
**target,
|
|
117
|
+
UploadId=upload_id,
|
|
118
|
+
MultipartUpload={"Parts": parts},
|
|
119
|
+
)
|
|
120
|
+
except BaseException:
|
|
121
|
+
try:
|
|
122
|
+
await s3.abort_multipart_upload(
|
|
123
|
+
**target, UploadId=upload_id
|
|
124
|
+
)
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
raise
|
|
86
128
|
except ClientError as exc:
|
|
87
129
|
raise self._translate(exc, "write") from exc
|
|
88
|
-
return StoredObject(
|
|
89
|
-
location=location,
|
|
90
|
-
size=len(content),
|
|
91
|
-
content_type=content_type,
|
|
92
|
-
)
|
|
130
|
+
return StoredObject(location=location, size=size, content_type=content_type)
|
|
93
131
|
|
|
94
132
|
async def read(self, location: StorageLocation) -> bytes:
|
|
95
133
|
try:
|
|
@@ -1276,6 +1276,28 @@ class KnowledgeFsEntryRepository:
|
|
|
1276
1276
|
row = await cursor.fetchone()
|
|
1277
1277
|
return int(row["file_count"])
|
|
1278
1278
|
|
|
1279
|
+
async def file_stats_by_knowledge_base_ids(
|
|
1280
|
+
self, cursor: Any, *, knowledge_base_ids: list[int]
|
|
1281
|
+
) -> dict[int, dict[str, int]]:
|
|
1282
|
+
"""Aggregate current live file counts and byte totals in one query."""
|
|
1283
|
+
await cursor.execute(
|
|
1284
|
+
"""SELECT knowledge_base_id, COUNT(*) AS file_count,
|
|
1285
|
+
COALESCE(SUM(COALESCE(file_size, 0)), 0) AS size_bytes
|
|
1286
|
+
FROM knowledge_fs_entry
|
|
1287
|
+
WHERE knowledge_base_id = ANY(%(knowledge_base_ids)s)
|
|
1288
|
+
AND is_deleted = FALSE
|
|
1289
|
+
AND entry_type = 'FILE'
|
|
1290
|
+
GROUP BY knowledge_base_id""",
|
|
1291
|
+
{"knowledge_base_ids": knowledge_base_ids},
|
|
1292
|
+
)
|
|
1293
|
+
return {
|
|
1294
|
+
int(row["knowledge_base_id"]): {
|
|
1295
|
+
"file_count": int(row["file_count"]),
|
|
1296
|
+
"size_bytes": int(row["size_bytes"]),
|
|
1297
|
+
}
|
|
1298
|
+
for row in await self._fetchall(cursor)
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1279
1301
|
async def list_file_entries_by_knowledge_base_id(
|
|
1280
1302
|
self, cursor, *, knowledge_base_id: int
|
|
1281
1303
|
) -> list[dict[str, Any]]:
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Internal disk-backed upload requests and bounded file operations."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import hashlib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from pydantic import Field, FilePath, field_validator
|
|
8
|
+
|
|
9
|
+
from by_qa.knowledge_base.api.schemas import KnowledgeItemUploadRequest
|
|
10
|
+
|
|
11
|
+
UPLOAD_CHUNK_SIZE = 1024 * 1024
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FileUploadRequest(KnowledgeItemUploadRequest):
|
|
15
|
+
"""Internal request; local paths are never accepted from the public API."""
|
|
16
|
+
|
|
17
|
+
file_content: FilePath = Field(validation_alias="fileContent")
|
|
18
|
+
|
|
19
|
+
@field_validator("file_content")
|
|
20
|
+
@classmethod
|
|
21
|
+
def nonempty_file(cls, value: Path) -> Path:
|
|
22
|
+
if value.stat().st_size == 0:
|
|
23
|
+
raise ValueError("fileContent must not be empty")
|
|
24
|
+
return value
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def file_io(function, *args):
|
|
28
|
+
"""Finish an outstanding disk operation before its temporary files are removed."""
|
|
29
|
+
task = asyncio.create_task(asyncio.to_thread(function, *args))
|
|
30
|
+
try:
|
|
31
|
+
return await asyncio.shield(task)
|
|
32
|
+
except asyncio.CancelledError:
|
|
33
|
+
await asyncio.gather(task, return_exceptions=True)
|
|
34
|
+
raise
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def file_checksum(source: Path) -> str:
|
|
38
|
+
digest = hashlib.sha256()
|
|
39
|
+
with source.open("rb") as stream:
|
|
40
|
+
while chunk := stream.read(UPLOAD_CHUNK_SIZE):
|
|
41
|
+
digest.update(chunk)
|
|
42
|
+
return digest.hexdigest()
|