by-qa 0.4.4.dev3__tar.gz → 0.4.4.dev4__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.dev4}/PKG-INFO +1 -1
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/pyproject.toml +1 -1
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/api/routes.py +161 -135
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/infrastructure/storage.py +4 -2
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/infrastructure/storage_s3.py +51 -13
- by_qa-0.4.4.dev4/src/by_qa/knowledge_base/services/file_upload_source.py +42 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_item_ingestion_service.py +31 -12
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/zip_batch_import_service.py +90 -72
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/uv.lock +1 -1
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/.env.example +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/.github/workflows/ci.yml +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/.github/workflows/release.yml +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/.gitignore +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/.pre-commit-config.yaml +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/AGENTS.md +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/CLAUDE.md +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/CONTEXT.md +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/Dockerfile +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/LICENSE +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/Makefile +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/README.md +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/README_zh.md +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/docker/minio/init/init-minio.sh +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/docker/opengauss/custom/Dockerfile +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/docker/opengauss/init/init-opengauss.sh +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/docker-compose.kb-stack.yml +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/eval/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/eval/cli.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/eval/judge.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/eval/models.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/eval/runner.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/pylintrc +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/pytest.ini +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/config.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/core/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/core/exceptions.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/core/framework_client.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/core/logger.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/core/model_config.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/api/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/api/knowledge_entity_schemas.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/api/metadata_schemas.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/api/schemas.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/build_status.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/dsl/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/dsl/compiler.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/dsl/errors.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/dsl/validator.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/events.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/infrastructure/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/infrastructure/database.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/infrastructure/object_storage.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/infrastructure/runtime.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/metadata_types.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/file_metadata_value_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_base_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_build_acceptance_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_build_batch_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_build_task_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_entity_asset_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_entity_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_fetch_cache_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_file_reference_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/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.dev4}/src/by_qa/knowledge_base/repositories/knowledge_fs_entry_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_item_chunk_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_item_search_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/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.dev4}/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.dev4}/src/by_qa/knowledge_base/repositories/metadata_search_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/processing_task_query_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/retrieval_projection_repository.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/bootstrap_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/cache_file_lock.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/document_update_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/embedding_query_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/entry_metadata.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/errors.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/file_build_background_runner.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/file_build_execution_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/file_build_models.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/file_build_mutation_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/file_build_processing_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/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.dev4}/src/by_qa/knowledge_base/services/file_metadata_query_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/file_metadata_update_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_base_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_document_metadata.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_entity_background_runner.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_entity_discovery.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_entity_enrichment.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_entity_intelligence.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_entity_processing_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_entity_synonym_resolution.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_entity_task_worker.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/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.dev4}/src/by_qa/knowledge_base/services/knowledge_item_search_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/markdown_front_matter.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/markdown_reference_resolver.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/markdown_reference_rewriter.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/markdown_update_summary_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/metadata_search_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/semantic_task_mutation_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/000_extensions.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/001_knowledge_base.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/002_knowledge_fs_entry.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/003_knowledge_chunk.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/004_retrieval_projection.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/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.dev4}/src/by_qa/knowledge_base/sql/006_knowledge_build_task.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/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.dev4}/src/by_qa/knowledge_base/sql/010_knowledge_chunk_indexes.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/011_retrieval_projection_indexes.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/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.dev4}/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.dev4}/src/by_qa/knowledge_base/sql/014_embedding_table.sql.tpl +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/015_metadata_property_def.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/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.dev4}/src/by_qa/knowledge_base/sql/017_file_metadata_value.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/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.dev4}/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.dev4}/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.dev4}/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.dev4}/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.dev4}/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.dev4}/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.dev4}/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.dev4}/src/by_qa/knowledge_base/sql/026_knowledge_file_reference.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/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.dev4}/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.dev4}/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.dev4}/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.dev4}/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.dev4}/src/by_qa/knowledge_base/sql/032_knowledge_schema_migration.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/033_knowledge_entity.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/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.dev4}/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.dev4}/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.dev4}/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.dev4}/src/by_qa/knowledge_base/sql/038_knowledge_build_batch.sql +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/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.dev4}/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.dev4}/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.dev4}/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.dev4}/src/by_qa/knowledge_build/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_build/runtime.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_build/services/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_build/services/document_chunking_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_build/services/heading_patterns.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_common/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_common/exceptions.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_common/kb_path_utils.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_common/markdown_reference.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_common/schemas.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_common/text_segmentation.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/main.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/answer_synthesizer.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/multi_hop_react.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/multi_hop_summarizer.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/query_decomposer.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/recursion_fallback.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/single_hop_react.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/standalone_question_rewriter.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/subanswer_aggregator.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/base_engine.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/config.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/context.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/context_manager.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/event_filter.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/exceptions.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/fallback_messages.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/messages.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/middleware/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/middleware/tool_call_guard.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/models.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/operation_registry.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/prompt_fragments.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/reducers.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/state.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/engine.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/graph.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/nodes/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/nodes/retrieve.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/state.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/types.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/engine.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/graph.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/nodes/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/nodes/final_answer.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/nodes/router.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/nodes/state_cleanup.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/state.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/types.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/services/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/services/checkpointer_factory.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/services/llm_service.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/services/opengauss_checkpointer.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/services/session_history.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/tools/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/tools/dsl_guide.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/tools/knowledge_tools.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/tools/operations/__init__.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/tools/operations/base.py +0 -0
- {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/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
|
|
|
@@ -64,6 +65,11 @@ from by_qa.knowledge_base.services.errors import (
|
|
|
64
65
|
KnowledgeBaseConfigurationError,
|
|
65
66
|
KnowledgeBaseValidationError,
|
|
66
67
|
)
|
|
68
|
+
from by_qa.knowledge_base.services.file_upload_source import (
|
|
69
|
+
UPLOAD_CHUNK_SIZE,
|
|
70
|
+
FileUploadRequest,
|
|
71
|
+
file_io,
|
|
72
|
+
)
|
|
67
73
|
from by_qa.knowledge_base.services.knowledge_item_ingestion_service import (
|
|
68
74
|
convert_uploaded_file_to_markdown,
|
|
69
75
|
)
|
|
@@ -756,148 +762,168 @@ def register_routes(
|
|
|
756
762
|
skip_if_duplicate: bool = Form(False, alias="skipIfDuplicate"),
|
|
757
763
|
metadata: str | None = Form(None, alias="metadata"),
|
|
758
764
|
):
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
"
|
|
767
|
-
|
|
768
|
-
|
|
765
|
+
async with AsyncExitStack() as stack:
|
|
766
|
+
try:
|
|
767
|
+
filename = (
|
|
768
|
+
(file_content.filename or "") if file_content is not None else ""
|
|
769
|
+
)
|
|
770
|
+
is_zip = extract_zip and filename.lower().endswith(".zip")
|
|
771
|
+
if is_zip and file_content is not None:
|
|
772
|
+
temp_dir = TemporaryDirectory(prefix="byqa-upload-")
|
|
773
|
+
stack.push_async_callback(file_io, temp_dir.cleanup)
|
|
774
|
+
payload = Path(temp_dir.name) / "upload.zip"
|
|
775
|
+
with payload.open("wb") as stream:
|
|
776
|
+
while chunk := await file_content.read(UPLOAD_CHUNK_SIZE):
|
|
777
|
+
await file_io(stream.write, chunk)
|
|
778
|
+
request_model = FileUploadRequest
|
|
779
|
+
else:
|
|
780
|
+
payload = (
|
|
781
|
+
await file_content.read() if file_content is not None else None
|
|
782
|
+
)
|
|
783
|
+
request_model = KnowledgeItemUploadRequest
|
|
784
|
+
request = request_model.model_validate(
|
|
785
|
+
{
|
|
786
|
+
"knCode": kn_code,
|
|
787
|
+
"filePath": file_path,
|
|
788
|
+
"fileDescription": file_description,
|
|
789
|
+
"fileContent": payload,
|
|
790
|
+
"mimeType": (
|
|
791
|
+
file_content.content_type
|
|
792
|
+
if file_content is not None
|
|
793
|
+
else None
|
|
794
|
+
),
|
|
795
|
+
"processFrontMatter": process_front_matter,
|
|
796
|
+
"extractZip": extract_zip,
|
|
797
|
+
"skipIfDuplicate": skip_if_duplicate,
|
|
798
|
+
"metadata": _parse_optional_metadata_form(metadata),
|
|
799
|
+
}
|
|
800
|
+
)
|
|
801
|
+
except (ValidationError, ValueError) as exc:
|
|
802
|
+
return _documented_error_response(
|
|
803
|
+
result_msg="request validation failed",
|
|
804
|
+
result_object=(
|
|
805
|
+
{"errors": json.loads(exc.json())}
|
|
806
|
+
if isinstance(exc, ValidationError)
|
|
807
|
+
else {"errors": [{"msg": str(exc)}]}
|
|
769
808
|
),
|
|
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,
|
|
809
|
+
status_code=422,
|
|
810
|
+
)
|
|
811
|
+
logger.info(
|
|
812
|
+
"upload_file request received: kb_code=%s, file_path=%s, has_description=%s, process_front_matter=%s, extract_zip=%s",
|
|
813
|
+
request.kb_code,
|
|
814
|
+
request.file_path,
|
|
815
|
+
request.file_description is not None,
|
|
816
|
+
request.process_front_matter,
|
|
817
|
+
request.extract_zip,
|
|
785
818
|
)
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
819
|
+
try:
|
|
820
|
+
service = await get_knowledge_item_ingestion_service()
|
|
821
|
+
if request.extract_zip and filename.lower().endswith(".zip"):
|
|
822
|
+
if not await file_io(zipfile.is_zipfile, payload):
|
|
823
|
+
return _documented_error_response(
|
|
824
|
+
result_msg="invalid zip file",
|
|
825
|
+
result_object={},
|
|
826
|
+
status_code=422,
|
|
827
|
+
)
|
|
828
|
+
directory_service = await get_knowledge_base_service()
|
|
829
|
+
batch_service = ZipBatchImportService(
|
|
830
|
+
ingestion_service=service,
|
|
831
|
+
directory_service=directory_service,
|
|
832
|
+
)
|
|
833
|
+
result = await batch_service.import_zip(
|
|
834
|
+
kb_code=request.kb_code,
|
|
835
|
+
target_dir=request.file_path,
|
|
836
|
+
zip_path=payload,
|
|
837
|
+
process_front_matter=request.process_front_matter,
|
|
838
|
+
file_description=request.file_description,
|
|
839
|
+
skip_if_duplicate=request.skip_if_duplicate,
|
|
840
|
+
metadata=request.metadata,
|
|
841
|
+
)
|
|
842
|
+
result_object = {
|
|
843
|
+
"data": [
|
|
844
|
+
item.model_dump(by_alias=True) for item in result.data
|
|
845
|
+
],
|
|
846
|
+
"summary": result.summary.model_dump(by_alias=True),
|
|
847
|
+
}
|
|
848
|
+
if result.post_process_errors:
|
|
849
|
+
result_object["postProcessErrors"] = result.post_process_errors
|
|
850
|
+
if result.summary.succeeded > 0:
|
|
851
|
+
await _try_schedule_event(
|
|
852
|
+
background_tasks,
|
|
853
|
+
lambda: build_resource_event(
|
|
854
|
+
event_type=ResourceEventType.FILE_IMPORTED,
|
|
855
|
+
kb_code=request.kb_code,
|
|
856
|
+
source_path=None,
|
|
857
|
+
target_path=_ensure_leading_slash(request.file_path),
|
|
858
|
+
items=[
|
|
859
|
+
{
|
|
860
|
+
"sourcePath": item.file_path,
|
|
861
|
+
"targetPath": (
|
|
862
|
+
item.file_path if item.success else None
|
|
863
|
+
),
|
|
864
|
+
"resourceType": item.resource_type,
|
|
865
|
+
"success": item.success,
|
|
866
|
+
"error": item.error,
|
|
867
|
+
}
|
|
868
|
+
for item in result.data
|
|
869
|
+
],
|
|
870
|
+
result=result.summary.model_dump(),
|
|
871
|
+
),
|
|
872
|
+
)
|
|
873
|
+
return _documented_success_response(result_object=result_object)
|
|
874
|
+
# single file
|
|
875
|
+
file_path_norm = "/" + request.file_path.strip("/")
|
|
876
|
+
segments = [s for s in file_path_norm.split("/") if s]
|
|
877
|
+
if any(s == ".." for s in segments):
|
|
799
878
|
return _documented_error_response(
|
|
800
|
-
result_msg="
|
|
801
|
-
result_object={},
|
|
802
|
-
status_code=422,
|
|
879
|
+
result_msg="unsafe path", result_object={}, status_code=422
|
|
803
880
|
)
|
|
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
|
-
)
|
|
881
|
+
await service.upload_file(request)
|
|
818
882
|
result_object = {
|
|
819
|
-
"data": [
|
|
820
|
-
|
|
883
|
+
"data": [
|
|
884
|
+
{
|
|
885
|
+
"filePath": file_path_norm,
|
|
886
|
+
"success": True,
|
|
887
|
+
"error": None,
|
|
888
|
+
}
|
|
889
|
+
],
|
|
890
|
+
"summary": {"total": 1, "succeeded": 1, "failed": 0},
|
|
821
891
|
}
|
|
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
|
-
)
|
|
892
|
+
await _try_schedule_event(
|
|
893
|
+
background_tasks,
|
|
894
|
+
lambda: build_resource_event(
|
|
895
|
+
event_type=ResourceEventType.FILE_IMPORTED,
|
|
896
|
+
kb_code=request.kb_code,
|
|
897
|
+
source_path=None,
|
|
898
|
+
target_path=file_path_norm,
|
|
899
|
+
result=result_object["summary"],
|
|
900
|
+
),
|
|
901
|
+
)
|
|
847
902
|
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):
|
|
903
|
+
except KnowledgeBaseConfigurationError as exc:
|
|
852
904
|
return _documented_error_response(
|
|
853
|
-
result_msg=
|
|
905
|
+
result_msg=str(exc),
|
|
906
|
+
result_object={},
|
|
907
|
+
status_code=503,
|
|
908
|
+
)
|
|
909
|
+
except KnowledgeBaseValidationError as exc:
|
|
910
|
+
return _documented_error_response(
|
|
911
|
+
result_msg=str(exc),
|
|
912
|
+
result_object={},
|
|
913
|
+
status_code=422,
|
|
914
|
+
)
|
|
915
|
+
except Exception as exc:
|
|
916
|
+
logger.exception(
|
|
917
|
+
"upload_file unexpected error: kb_code=%s, file_path=%s, error=%s",
|
|
918
|
+
request.kb_code,
|
|
919
|
+
request.file_path,
|
|
920
|
+
exc,
|
|
921
|
+
)
|
|
922
|
+
return _documented_error_response(
|
|
923
|
+
result_msg=str(exc) or "internal error",
|
|
924
|
+
result_object={},
|
|
925
|
+
status_code=500,
|
|
854
926
|
)
|
|
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
927
|
|
|
902
928
|
@app.post("/api/v1/knowledgeItems/update")
|
|
903
929
|
@app.post("/api/v1/knowledge-items/update")
|
|
@@ -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:
|
|
@@ -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()
|
|
@@ -5,7 +5,7 @@ import hashlib
|
|
|
5
5
|
import mimetypes
|
|
6
6
|
from collections.abc import Iterable, Mapping
|
|
7
7
|
from dataclasses import dataclass, field
|
|
8
|
-
from pathlib import PurePosixPath
|
|
8
|
+
from pathlib import Path, PurePosixPath
|
|
9
9
|
from typing import Any, Callable
|
|
10
10
|
|
|
11
11
|
from by_qa.core import logger
|
|
@@ -37,6 +37,11 @@ from by_qa.knowledge_base.services.errors import KnowledgeBaseValidationError
|
|
|
37
37
|
from by_qa.knowledge_base.services.file_build_execution_service import (
|
|
38
38
|
FileBuildTerminalError,
|
|
39
39
|
)
|
|
40
|
+
from by_qa.knowledge_base.services.file_upload_source import (
|
|
41
|
+
FileUploadRequest,
|
|
42
|
+
file_checksum,
|
|
43
|
+
file_io,
|
|
44
|
+
)
|
|
40
45
|
from by_qa.knowledge_base.services.knowledge_document_metadata import (
|
|
41
46
|
ensure_document_kind_metadata,
|
|
42
47
|
)
|
|
@@ -175,13 +180,22 @@ class KnowledgeItemIngestionService:
|
|
|
175
180
|
document_chunking_service=document_chunking_service,
|
|
176
181
|
)
|
|
177
182
|
|
|
178
|
-
async def upload_file(
|
|
183
|
+
async def upload_file(
|
|
184
|
+
self, request: KnowledgeItemUploadRequest | FileUploadRequest
|
|
185
|
+
) -> dict[str, Any]:
|
|
179
186
|
"""Upload one original file and register its storage metadata on the file entry."""
|
|
187
|
+
source = request.file_content
|
|
188
|
+
source_size = source.stat().st_size if isinstance(source, Path) else len(source)
|
|
189
|
+
checksum = (
|
|
190
|
+
await file_io(file_checksum, source)
|
|
191
|
+
if isinstance(source, Path)
|
|
192
|
+
else hashlib.sha256(source).hexdigest()
|
|
193
|
+
)
|
|
180
194
|
logger.info(
|
|
181
195
|
"knowledge_item_ingestion_service.upload_file started: kb_code=%s, file_path=%s, file_size=%s",
|
|
182
196
|
request.kb_code,
|
|
183
197
|
request.file_path,
|
|
184
|
-
|
|
198
|
+
source_size,
|
|
185
199
|
)
|
|
186
200
|
normalized_file_path = request.file_path.strip()
|
|
187
201
|
if not normalized_file_path:
|
|
@@ -231,7 +245,6 @@ class KnowledgeItemIngestionService:
|
|
|
231
245
|
)
|
|
232
246
|
await connection.commit()
|
|
233
247
|
|
|
234
|
-
checksum = hashlib.sha256(request.file_content).hexdigest()
|
|
235
248
|
await self.knowledge_fs_entry_repository.lock_checksum_scope(
|
|
236
249
|
cursor,
|
|
237
250
|
knowledge_base_id=knowledge_base_id,
|
|
@@ -265,8 +278,11 @@ class KnowledgeItemIngestionService:
|
|
|
265
278
|
raise KnowledgeBaseValidationError(str(exc)) from exc
|
|
266
279
|
|
|
267
280
|
fs_entry_id = self._row_id(file_entry_row)
|
|
268
|
-
content =
|
|
281
|
+
content = source
|
|
282
|
+
original_content = source
|
|
269
283
|
if self._is_markdown_upload(normalized_object_path, mime_type):
|
|
284
|
+
if isinstance(source, Path):
|
|
285
|
+
original_content = content = await file_io(source.read_bytes)
|
|
270
286
|
_, content = split_front_matter(content)
|
|
271
287
|
content = await self._rewrite_markdown_references(
|
|
272
288
|
content,
|
|
@@ -285,17 +301,16 @@ class KnowledgeItemIngestionService:
|
|
|
285
301
|
)
|
|
286
302
|
|
|
287
303
|
stored = await self.storage_provider.write(
|
|
288
|
-
original_location,
|
|
289
|
-
content,
|
|
290
|
-
content_type=mime_type,
|
|
304
|
+
original_location, content, content_type=mime_type
|
|
291
305
|
)
|
|
306
|
+
stored_size = source_size if isinstance(content, Path) else len(content)
|
|
292
307
|
|
|
293
308
|
await self.knowledge_fs_entry_repository.update_file_entry_storage(
|
|
294
309
|
cursor,
|
|
295
310
|
fs_entry_id=fs_entry_id,
|
|
296
311
|
file_description=request.file_description,
|
|
297
312
|
original_location=stored.location,
|
|
298
|
-
file_size=
|
|
313
|
+
file_size=stored_size,
|
|
299
314
|
mime_type=mime_type,
|
|
300
315
|
checksum=checksum,
|
|
301
316
|
)
|
|
@@ -304,7 +319,7 @@ class KnowledgeItemIngestionService:
|
|
|
304
319
|
if request.process_front_matter and self._is_markdown_upload(
|
|
305
320
|
normalized_object_path, mime_type
|
|
306
321
|
):
|
|
307
|
-
front_matter = parse_front_matter(
|
|
322
|
+
front_matter = parse_front_matter(original_content)
|
|
308
323
|
await upsert_entry_metadata(
|
|
309
324
|
cursor,
|
|
310
325
|
metadata_repository=self.file_metadata_value_repository,
|
|
@@ -345,7 +360,7 @@ class KnowledgeItemIngestionService:
|
|
|
345
360
|
await connection.close()
|
|
346
361
|
|
|
347
362
|
async def find_duplicate_file(
|
|
348
|
-
self, *, kb_code: str, file_content: bytes
|
|
363
|
+
self, *, kb_code: str, file_content: bytes | Path
|
|
349
364
|
) -> str | None:
|
|
350
365
|
"""Return the path of a same-checksum file for guarded batch imports."""
|
|
351
366
|
connection = await self.connection_factory()
|
|
@@ -357,7 +372,11 @@ class KnowledgeItemIngestionService:
|
|
|
357
372
|
f"knowledge base not found: {kb_code}"
|
|
358
373
|
)
|
|
359
374
|
knowledge_base_id = self._row_id(kb_row)
|
|
360
|
-
checksum =
|
|
375
|
+
checksum = (
|
|
376
|
+
await file_io(file_checksum, file_content)
|
|
377
|
+
if isinstance(file_content, Path)
|
|
378
|
+
else hashlib.sha256(file_content).hexdigest()
|
|
379
|
+
)
|
|
361
380
|
await self.knowledge_fs_entry_repository.lock_checksum_scope(
|
|
362
381
|
cursor,
|
|
363
382
|
knowledge_base_id=knowledge_base_id,
|