by-qa 0.4.4.dev2__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.
Files changed (214) hide show
  1. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/PKG-INFO +1 -1
  2. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/pyproject.toml +1 -1
  3. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/api/routes.py +226 -142
  4. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/api/schemas.py +11 -0
  5. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/infrastructure/storage.py +4 -2
  6. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/infrastructure/storage_s3.py +51 -13
  7. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_fs_entry_repository.py +15 -0
  8. by_qa-0.4.4.dev4/src/by_qa/knowledge_base/services/file_upload_source.py +42 -0
  9. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_base_service.py +24 -1
  10. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_item_ingestion_service.py +31 -12
  11. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/zip_batch_import_service.py +90 -72
  12. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/uv.lock +1 -1
  13. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/.env.example +0 -0
  14. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/.github/workflows/ci.yml +0 -0
  15. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/.github/workflows/release.yml +0 -0
  16. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/.gitignore +0 -0
  17. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/.pre-commit-config.yaml +0 -0
  18. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/AGENTS.md +0 -0
  19. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/CLAUDE.md +0 -0
  20. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/CONTEXT.md +0 -0
  21. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/Dockerfile +0 -0
  22. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/LICENSE +0 -0
  23. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/Makefile +0 -0
  24. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/README.md +0 -0
  25. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/README_zh.md +0 -0
  26. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/docker/minio/init/init-minio.sh +0 -0
  27. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/docker/opengauss/custom/Dockerfile +0 -0
  28. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/docker/opengauss/init/init-opengauss.sh +0 -0
  29. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/docker-compose.kb-stack.yml +0 -0
  30. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/eval/__init__.py +0 -0
  31. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/eval/cli.py +0 -0
  32. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/eval/judge.py +0 -0
  33. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/eval/models.py +0 -0
  34. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/eval/runner.py +0 -0
  35. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/pylintrc +0 -0
  36. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/pytest.ini +0 -0
  37. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/__init__.py +0 -0
  38. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/config.py +0 -0
  39. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/core/__init__.py +0 -0
  40. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/core/exceptions.py +0 -0
  41. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/core/framework_client.py +0 -0
  42. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/core/logger.py +0 -0
  43. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/core/model_config.py +0 -0
  44. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/__init__.py +0 -0
  45. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/api/__init__.py +0 -0
  46. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/api/knowledge_entity_schemas.py +0 -0
  47. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/api/metadata_schemas.py +0 -0
  48. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/build_status.py +0 -0
  49. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/dsl/__init__.py +0 -0
  50. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/dsl/compiler.py +0 -0
  51. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/dsl/errors.py +0 -0
  52. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/dsl/validator.py +0 -0
  53. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/events.py +0 -0
  54. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/infrastructure/__init__.py +0 -0
  55. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/infrastructure/database.py +0 -0
  56. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/infrastructure/object_storage.py +0 -0
  57. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/infrastructure/runtime.py +0 -0
  58. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/metadata_types.py +0 -0
  59. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/__init__.py +0 -0
  60. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/file_metadata_value_repository.py +0 -0
  61. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_base_repository.py +0 -0
  62. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_build_acceptance_repository.py +0 -0
  63. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_build_batch_repository.py +0 -0
  64. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_build_task_repository.py +0 -0
  65. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_entity_asset_repository.py +0 -0
  66. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_entity_repository.py +0 -0
  67. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_fetch_cache_repository.py +0 -0
  68. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_file_reference_repository.py +0 -0
  69. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_file_update_timeline_repository.py +0 -0
  70. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_item_chunk_repository.py +0 -0
  71. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_item_search_repository.py +0 -0
  72. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_semantic_processing_batch_repository.py +0 -0
  73. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_semantic_processing_task_repository.py +0 -0
  74. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/metadata_search_repository.py +0 -0
  75. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/processing_task_query_repository.py +0 -0
  76. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/retrieval_projection_repository.py +0 -0
  77. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/__init__.py +0 -0
  78. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/bootstrap_service.py +0 -0
  79. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/cache_file_lock.py +0 -0
  80. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/document_update_service.py +0 -0
  81. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/embedding_query_service.py +0 -0
  82. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/entry_metadata.py +0 -0
  83. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/errors.py +0 -0
  84. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/file_build_background_runner.py +0 -0
  85. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/file_build_execution_service.py +0 -0
  86. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/file_build_models.py +0 -0
  87. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/file_build_mutation_service.py +0 -0
  88. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/file_build_processing_service.py +0 -0
  89. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/file_build_terminal_event_service.py +0 -0
  90. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/file_metadata_query_service.py +0 -0
  91. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/file_metadata_update_service.py +0 -0
  92. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_document_metadata.py +0 -0
  93. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_entity_background_runner.py +0 -0
  94. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_entity_discovery.py +0 -0
  95. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_entity_enrichment.py +0 -0
  96. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_entity_intelligence.py +0 -0
  97. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_entity_processing_service.py +0 -0
  98. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_entity_synonym_resolution.py +0 -0
  99. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_entity_task_worker.py +0 -0
  100. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_fetch_cache_cleanup_service.py +0 -0
  101. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_item_search_service.py +0 -0
  102. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/markdown_front_matter.py +0 -0
  103. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/markdown_reference_resolver.py +0 -0
  104. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/markdown_reference_rewriter.py +0 -0
  105. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/markdown_update_summary_service.py +0 -0
  106. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/metadata_search_service.py +0 -0
  107. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/semantic_task_mutation_service.py +0 -0
  108. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/000_extensions.sql +0 -0
  109. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/001_knowledge_base.sql +0 -0
  110. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/002_knowledge_fs_entry.sql +0 -0
  111. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/003_knowledge_chunk.sql +0 -0
  112. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/004_retrieval_projection.sql +0 -0
  113. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/005_knowledge_fetch_cache_index.sql +0 -0
  114. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/006_knowledge_build_task.sql +0 -0
  115. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/009_knowledge_fs_entry_indexes.sql +0 -0
  116. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/010_knowledge_chunk_indexes.sql +0 -0
  117. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/011_retrieval_projection_indexes.sql +0 -0
  118. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/012_knowledge_fetch_cache_index_indexes.sql +0 -0
  119. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/013_knowledge_build_task_indexes.sql +0 -0
  120. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/014_embedding_table.sql.tpl +0 -0
  121. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/015_metadata_property_def.sql +0 -0
  122. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/016_metadata_property_def_indexes.sql +0 -0
  123. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/017_file_metadata_value.sql +0 -0
  124. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/018_file_metadata_value_indexes.sql +0 -0
  125. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/019_knowledge_fs_entry_virtual_path.sql +0 -0
  126. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/020_knowledge_fs_entry_virtual_path_indexes.sql +0 -0
  127. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/021_knowledge_fs_entry_virtual_path_backfill.sql +0 -0
  128. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/022_knowledge_fs_entry_top_level_sibling_unique.sql +0 -0
  129. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/023_metadata_property_def_name_unique.sql +0 -0
  130. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/024_knowledge_base_name_unique.sql +0 -0
  131. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/025_metadata_value_self_contained_migration.sql +0 -0
  132. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/026_knowledge_file_reference.sql +0 -0
  133. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/027_knowledge_file_update_timeline.sql +0 -0
  134. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/028_knowledge_fs_entry_checksum_index.sql +0 -0
  135. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/029_knowledge_semantic_processing_task.sql +0 -0
  136. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/030_knowledge_file_reference_semantic_extension.sql +0 -0
  137. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/031_knowledge_file_document_kind_backfill.sql +0 -0
  138. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/032_knowledge_schema_migration.sql +0 -0
  139. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/033_knowledge_entity.sql +0 -0
  140. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/034_knowledge_entity_embedding_table.sql.tpl +0 -0
  141. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/035_knowledge_entity_discovery_schema.sql +0 -0
  142. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/036_knowledge_entity_embedding_full_only.sql.tpl +0 -0
  143. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/037_knowledge_semantic_processing_directory_scope.sql +0 -0
  144. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/038_knowledge_build_batch.sql +0 -0
  145. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/039_knowledge_build_task_background_extension.sql +0 -0
  146. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/040_knowledge_build_task_legacy_backfill.sql.tpl +0 -0
  147. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/041_knowledge_build_task_active_constraints.sql +0 -0
  148. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/042_knowledge_fs_entry_upload_uniqueness.sql +0 -0
  149. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_build/__init__.py +0 -0
  150. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_build/runtime.py +0 -0
  151. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_build/services/__init__.py +0 -0
  152. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_build/services/document_chunking_service.py +0 -0
  153. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_build/services/heading_patterns.py +0 -0
  154. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_common/__init__.py +0 -0
  155. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_common/exceptions.py +0 -0
  156. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_common/kb_path_utils.py +0 -0
  157. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_common/markdown_reference.py +0 -0
  158. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_common/schemas.py +0 -0
  159. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_common/text_segmentation.py +0 -0
  160. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/main.py +0 -0
  161. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/__init__.py +0 -0
  162. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/__init__.py +0 -0
  163. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/answer_synthesizer.py +0 -0
  164. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/multi_hop_react.py +0 -0
  165. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/multi_hop_summarizer.py +0 -0
  166. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/query_decomposer.py +0 -0
  167. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/recursion_fallback.py +0 -0
  168. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/single_hop_react.py +0 -0
  169. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/standalone_question_rewriter.py +0 -0
  170. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/subanswer_aggregator.py +0 -0
  171. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/__init__.py +0 -0
  172. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/base_engine.py +0 -0
  173. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/config.py +0 -0
  174. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/context.py +0 -0
  175. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/context_manager.py +0 -0
  176. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/event_filter.py +0 -0
  177. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/exceptions.py +0 -0
  178. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/fallback_messages.py +0 -0
  179. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/messages.py +0 -0
  180. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/middleware/__init__.py +0 -0
  181. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/middleware/tool_call_guard.py +0 -0
  182. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/models.py +0 -0
  183. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/operation_registry.py +0 -0
  184. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/prompt_fragments.py +0 -0
  185. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/reducers.py +0 -0
  186. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/state.py +0 -0
  187. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/__init__.py +0 -0
  188. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/__init__.py +0 -0
  189. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/engine.py +0 -0
  190. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/graph.py +0 -0
  191. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/nodes/__init__.py +0 -0
  192. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/nodes/retrieve.py +0 -0
  193. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/state.py +0 -0
  194. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/types.py +0 -0
  195. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/__init__.py +0 -0
  196. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/engine.py +0 -0
  197. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/graph.py +0 -0
  198. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/nodes/__init__.py +0 -0
  199. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/nodes/final_answer.py +0 -0
  200. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/nodes/router.py +0 -0
  201. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/nodes/state_cleanup.py +0 -0
  202. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/state.py +0 -0
  203. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/types.py +0 -0
  204. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/services/__init__.py +0 -0
  205. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/services/checkpointer_factory.py +0 -0
  206. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/services/llm_service.py +0 -0
  207. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/services/opengauss_checkpointer.py +0 -0
  208. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/services/session_history.py +0 -0
  209. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/tools/__init__.py +0 -0
  210. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/tools/dsl_guide.py +0 -0
  211. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/tools/knowledge_tools.py +0 -0
  212. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/tools/operations/__init__.py +0 -0
  213. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/tools/operations/base.py +0 -0
  214. {by_qa-0.4.4.dev2 → by_qa-0.4.4.dev4}/src/by_qa/qa/tools/operations/knowledge_search.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: by-qa
3
- Version: 0.4.4.dev2
3
+ Version: 0.4.4.dev4
4
4
  Summary: Open-source knowledge-base ingestion and retrieval service.
5
5
  Author: AI Assistant
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "by-qa"
7
- version = "0.4.4.dev2"
7
+ version = "0.4.4.dev4"
8
8
  description = "Open-source knowledge-base ingestion and retrieval service."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -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
 
@@ -42,6 +43,7 @@ from by_qa.knowledge_base.api.schemas import (
42
43
  DocumentUpdateRequest,
43
44
  FileBuildStatusRequest,
44
45
  FileToMarkdownIndexRequest,
46
+ KnowledgeBaseFileCountRequest,
45
47
  KnowledgeItemDownloadRequest,
46
48
  KnowledgeItemGlobRequest,
47
49
  KnowledgeItemListDirRequest,
@@ -63,6 +65,11 @@ from by_qa.knowledge_base.services.errors import (
63
65
  KnowledgeBaseConfigurationError,
64
66
  KnowledgeBaseValidationError,
65
67
  )
68
+ from by_qa.knowledge_base.services.file_upload_source import (
69
+ UPLOAD_CHUNK_SIZE,
70
+ FileUploadRequest,
71
+ file_io,
72
+ )
66
73
  from by_qa.knowledge_base.services.knowledge_item_ingestion_service import (
67
74
  convert_uploaded_file_to_markdown,
68
75
  )
@@ -98,15 +105,17 @@ def _documented_error_response(
98
105
  result_msg: str,
99
106
  result_object: dict[str, Any] | None = None,
100
107
  status_code: int = 422,
108
+ preserve_http_status: bool = False,
101
109
  ) -> JSONResponse:
102
- """Return the documented error envelope with HTTP status normalized to 200."""
103
- logger.info(
104
- "knowledge_base error response normalized to HTTP 200: business_status_code=%s, result_msg=%s",
105
- status_code,
106
- result_msg,
107
- )
110
+ """Return the error envelope, normalizing legacy endpoints to HTTP 200."""
111
+ if not preserve_http_status:
112
+ logger.info(
113
+ "knowledge_base error response normalized to HTTP 200: business_status_code=%s, result_msg=%s",
114
+ status_code,
115
+ result_msg,
116
+ )
108
117
  return JSONResponse(
109
- status_code=200,
118
+ status_code=status_code if preserve_http_status else 200,
110
119
  content=jsonable_encoder(
111
120
  {
112
121
  "resultCode": "-1",
@@ -501,6 +510,61 @@ def register_routes(
501
510
  )
502
511
  return _documented_success_response(result_object={})
503
512
 
513
+ @app.post(
514
+ "/api/v1/knowledgeBases/fileCount",
515
+ openapi_extra={
516
+ "requestBody": {
517
+ "required": True,
518
+ "content": {
519
+ "application/json": {
520
+ "schema": KnowledgeBaseFileCountRequest.model_json_schema(
521
+ by_alias=True
522
+ )
523
+ }
524
+ },
525
+ }
526
+ },
527
+ )
528
+ async def knowledge_base_file_count(http_request: Request):
529
+ # Parse locally so malformed bodies also honor this endpoint's HTTP 422
530
+ # contract rather than the application's legacy HTTP 200 error handler.
531
+ try:
532
+ request = KnowledgeBaseFileCountRequest.model_validate(
533
+ await http_request.json()
534
+ )
535
+ except ValueError:
536
+ return _documented_error_response(
537
+ result_msg="request validation failed",
538
+ status_code=422,
539
+ preserve_http_status=True,
540
+ )
541
+
542
+ logger.info(
543
+ "knowledge_base_file_count request received: kb_code=%s", request.kb_code
544
+ )
545
+ try:
546
+ service = await get_knowledge_base_service()
547
+ result = await service.file_count(request)
548
+ except KnowledgeBaseConfigurationError as exc:
549
+ return _documented_error_response(
550
+ result_msg=str(exc), status_code=503, preserve_http_status=True
551
+ )
552
+ except KnowledgeBaseValidationError as exc:
553
+ return _documented_error_response(
554
+ result_msg=str(exc), status_code=422, preserve_http_status=True
555
+ )
556
+ except Exception as exc:
557
+ logger.exception(
558
+ "knowledge_base_file_count unexpected error: kb_code=%s",
559
+ request.kb_code,
560
+ )
561
+ return _documented_error_response(
562
+ result_msg=str(exc) or "internal error",
563
+ status_code=500,
564
+ preserve_http_status=True,
565
+ )
566
+ return _documented_success_response(result_object=result)
567
+
504
568
  @app.post("/api/v1/directories/create")
505
569
  async def create_directory(
506
570
  background_tasks: BackgroundTasks,
@@ -698,148 +762,168 @@ def register_routes(
698
762
  skip_if_duplicate: bool = Form(False, alias="skipIfDuplicate"),
699
763
  metadata: str | None = Form(None, alias="metadata"),
700
764
  ):
701
- try:
702
- payload = await file_content.read() if file_content is not None else None
703
- request = KnowledgeItemUploadRequest.model_validate(
704
- {
705
- "knCode": kn_code,
706
- "filePath": file_path,
707
- "fileDescription": file_description,
708
- "fileContent": payload,
709
- "mimeType": (
710
- file_content.content_type if file_content is not None else None
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)}]}
711
808
  ),
712
- "processFrontMatter": process_front_matter,
713
- "extractZip": extract_zip,
714
- "skipIfDuplicate": skip_if_duplicate,
715
- "metadata": _parse_optional_metadata_form(metadata),
716
- }
717
- )
718
- except (ValidationError, ValueError) as exc:
719
- return _documented_error_response(
720
- result_msg="request validation failed",
721
- result_object=(
722
- {"errors": json.loads(exc.json())}
723
- if isinstance(exc, ValidationError)
724
- else {"errors": [{"msg": str(exc)}]}
725
- ),
726
- 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,
727
818
  )
728
- logger.info(
729
- "upload_file request received: kb_code=%s, file_path=%s, has_description=%s, process_front_matter=%s, extract_zip=%s",
730
- request.kb_code,
731
- request.file_path,
732
- request.file_description is not None,
733
- request.process_front_matter,
734
- request.extract_zip,
735
- )
736
- filename = (file_content.filename or "") if file_content is not None else ""
737
- try:
738
- service = await get_knowledge_item_ingestion_service()
739
- if request.extract_zip and filename.lower().endswith(".zip"):
740
- if not zipfile.is_zipfile(io.BytesIO(payload or b"")):
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):
741
878
  return _documented_error_response(
742
- result_msg="invalid zip file",
743
- result_object={},
744
- status_code=422,
879
+ result_msg="unsafe path", result_object={}, status_code=422
745
880
  )
746
- directory_service = await get_knowledge_base_service()
747
- batch_service = ZipBatchImportService(
748
- ingestion_service=service,
749
- directory_service=directory_service,
750
- )
751
- result = await batch_service.import_zip(
752
- kb_code=request.kb_code,
753
- target_dir=request.file_path,
754
- zip_bytes=payload,
755
- process_front_matter=request.process_front_matter,
756
- file_description=request.file_description,
757
- skip_if_duplicate=request.skip_if_duplicate,
758
- metadata=request.metadata,
759
- )
881
+ await service.upload_file(request)
760
882
  result_object = {
761
- "data": [item.model_dump(by_alias=True) for item in result.data],
762
- "summary": result.summary.model_dump(by_alias=True),
883
+ "data": [
884
+ {
885
+ "filePath": file_path_norm,
886
+ "success": True,
887
+ "error": None,
888
+ }
889
+ ],
890
+ "summary": {"total": 1, "succeeded": 1, "failed": 0},
763
891
  }
764
- if result.post_process_errors:
765
- result_object["postProcessErrors"] = result.post_process_errors
766
- if result.summary.succeeded > 0:
767
- await _try_schedule_event(
768
- background_tasks,
769
- lambda: build_resource_event(
770
- event_type=ResourceEventType.FILE_IMPORTED,
771
- kb_code=request.kb_code,
772
- source_path=None,
773
- target_path=_ensure_leading_slash(request.file_path),
774
- items=[
775
- {
776
- "sourcePath": item.file_path,
777
- "targetPath": (
778
- item.file_path if item.success else None
779
- ),
780
- "resourceType": item.resource_type,
781
- "success": item.success,
782
- "error": item.error,
783
- }
784
- for item in result.data
785
- ],
786
- result=result.summary.model_dump(),
787
- ),
788
- )
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
+ )
789
902
  return _documented_success_response(result_object=result_object)
790
- # single file
791
- file_path_norm = "/" + request.file_path.strip("/")
792
- segments = [s for s in file_path_norm.split("/") if s]
793
- if any(s == ".." for s in segments):
903
+ except KnowledgeBaseConfigurationError as exc:
794
904
  return _documented_error_response(
795
- result_msg="unsafe path", result_object={}, status_code=422
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,
796
926
  )
797
- await service.upload_file(request)
798
- result_object = {
799
- "data": [
800
- {
801
- "filePath": file_path_norm,
802
- "success": True,
803
- "error": None,
804
- }
805
- ],
806
- "summary": {"total": 1, "succeeded": 1, "failed": 0},
807
- }
808
- await _try_schedule_event(
809
- background_tasks,
810
- lambda: build_resource_event(
811
- event_type=ResourceEventType.FILE_IMPORTED,
812
- kb_code=request.kb_code,
813
- source_path=None,
814
- target_path=file_path_norm,
815
- result=result_object["summary"],
816
- ),
817
- )
818
- return _documented_success_response(result_object=result_object)
819
- except KnowledgeBaseConfigurationError as exc:
820
- return _documented_error_response(
821
- result_msg=str(exc),
822
- result_object={},
823
- status_code=503,
824
- )
825
- except KnowledgeBaseValidationError as exc:
826
- return _documented_error_response(
827
- result_msg=str(exc),
828
- result_object={},
829
- status_code=422,
830
- )
831
- except Exception as exc:
832
- logger.exception(
833
- "upload_file unexpected error: kb_code=%s, file_path=%s, error=%s",
834
- request.kb_code,
835
- request.file_path,
836
- exc,
837
- )
838
- return _documented_error_response(
839
- result_msg=str(exc) or "internal error",
840
- result_object={},
841
- status_code=500,
842
- )
843
927
 
844
928
  @app.post("/api/v1/knowledgeItems/update")
845
929
  @app.post("/api/v1/knowledge-items/update")
@@ -54,6 +54,17 @@ class DeleteKnowledgeBaseRequest(BaseModel):
54
54
  )
55
55
 
56
56
 
57
+ class KnowledgeBaseFileCountRequest(BaseModel):
58
+ """Request body for counting all live files in a knowledge base."""
59
+
60
+ model_config = ConfigDict(populate_by_name=True)
61
+
62
+ kb_code: str = Field(
63
+ min_length=1,
64
+ validation_alias=AliasChoices("knCode", "kb_code"),
65
+ )
66
+
67
+
57
68
  class UpdateKnowledgeBaseRequest(BaseModel):
58
69
  """Request body for updating knowledge base business fields."""
59
70
 
@@ -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
- await s3.put_object(
81
- Bucket=location.namespace,
82
- Key=location.key,
83
- Body=content,
84
- ContentType=content_type,
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:
@@ -1261,6 +1261,21 @@ class KnowledgeFsEntryRepository:
1261
1261
  )
1262
1262
  return [dict(row) for row in await self._fetchall(cursor)]
1263
1263
 
1264
+ async def count_file_entries_by_knowledge_base_id(
1265
+ self, cursor, *, knowledge_base_id: int
1266
+ ) -> int:
1267
+ """Count all live files, regardless of directory depth or build state."""
1268
+ await cursor.execute(
1269
+ """SELECT COUNT(*) AS file_count
1270
+ FROM knowledge_fs_entry
1271
+ WHERE knowledge_base_id = %(knowledge_base_id)s
1272
+ AND is_deleted = FALSE
1273
+ AND entry_type = 'FILE'""",
1274
+ {"knowledge_base_id": knowledge_base_id},
1275
+ )
1276
+ row = await cursor.fetchone()
1277
+ return int(row["file_count"])
1278
+
1264
1279
  async def list_file_entries_by_knowledge_base_id(
1265
1280
  self, cursor, *, knowledge_base_id: int
1266
1281
  ) -> 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()