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.
Files changed (214) hide show
  1. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/PKG-INFO +1 -1
  2. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/pyproject.toml +1 -1
  3. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/api/routes.py +161 -135
  4. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/infrastructure/storage.py +4 -2
  5. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/infrastructure/storage_s3.py +51 -13
  6. by_qa-0.4.4.dev4/src/by_qa/knowledge_base/services/file_upload_source.py +42 -0
  7. {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
  8. {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
  9. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/uv.lock +1 -1
  10. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/.env.example +0 -0
  11. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/.github/workflows/ci.yml +0 -0
  12. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/.github/workflows/release.yml +0 -0
  13. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/.gitignore +0 -0
  14. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/.pre-commit-config.yaml +0 -0
  15. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/AGENTS.md +0 -0
  16. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/CLAUDE.md +0 -0
  17. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/CONTEXT.md +0 -0
  18. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/Dockerfile +0 -0
  19. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/LICENSE +0 -0
  20. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/Makefile +0 -0
  21. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/README.md +0 -0
  22. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/README_zh.md +0 -0
  23. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/docker/minio/init/init-minio.sh +0 -0
  24. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/docker/opengauss/custom/Dockerfile +0 -0
  25. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/docker/opengauss/init/init-opengauss.sh +0 -0
  26. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/docker-compose.kb-stack.yml +0 -0
  27. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/eval/__init__.py +0 -0
  28. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/eval/cli.py +0 -0
  29. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/eval/judge.py +0 -0
  30. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/eval/models.py +0 -0
  31. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/eval/runner.py +0 -0
  32. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/pylintrc +0 -0
  33. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/pytest.ini +0 -0
  34. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/__init__.py +0 -0
  35. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/config.py +0 -0
  36. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/core/__init__.py +0 -0
  37. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/core/exceptions.py +0 -0
  38. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/core/framework_client.py +0 -0
  39. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/core/logger.py +0 -0
  40. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/core/model_config.py +0 -0
  41. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/__init__.py +0 -0
  42. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/api/__init__.py +0 -0
  43. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/api/knowledge_entity_schemas.py +0 -0
  44. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/api/metadata_schemas.py +0 -0
  45. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/api/schemas.py +0 -0
  46. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/build_status.py +0 -0
  47. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/dsl/__init__.py +0 -0
  48. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/dsl/compiler.py +0 -0
  49. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/dsl/errors.py +0 -0
  50. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/dsl/validator.py +0 -0
  51. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/events.py +0 -0
  52. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/infrastructure/__init__.py +0 -0
  53. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/infrastructure/database.py +0 -0
  54. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/infrastructure/object_storage.py +0 -0
  55. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/infrastructure/runtime.py +0 -0
  56. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/metadata_types.py +0 -0
  57. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/__init__.py +0 -0
  58. {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
  59. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_base_repository.py +0 -0
  60. {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
  61. {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
  62. {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
  63. {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
  64. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/knowledge_entity_repository.py +0 -0
  65. {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
  66. {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
  67. {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
  68. {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
  69. {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
  70. {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
  71. {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
  72. {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
  73. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/metadata_search_repository.py +0 -0
  74. {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
  75. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/repositories/retrieval_projection_repository.py +0 -0
  76. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/__init__.py +0 -0
  77. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/bootstrap_service.py +0 -0
  78. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/cache_file_lock.py +0 -0
  79. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/document_update_service.py +0 -0
  80. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/embedding_query_service.py +0 -0
  81. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/entry_metadata.py +0 -0
  82. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/errors.py +0 -0
  83. {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
  84. {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
  85. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/file_build_models.py +0 -0
  86. {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
  87. {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
  88. {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
  89. {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
  90. {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
  91. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_base_service.py +0 -0
  92. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_document_metadata.py +0 -0
  93. {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
  94. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_entity_discovery.py +0 -0
  95. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_entity_enrichment.py +0 -0
  96. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/knowledge_entity_intelligence.py +0 -0
  97. {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
  98. {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
  99. {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
  100. {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
  101. {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
  102. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/markdown_front_matter.py +0 -0
  103. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/markdown_reference_resolver.py +0 -0
  104. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/markdown_reference_rewriter.py +0 -0
  105. {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
  106. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/services/metadata_search_service.py +0 -0
  107. {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
  108. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/000_extensions.sql +0 -0
  109. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/001_knowledge_base.sql +0 -0
  110. {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
  111. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/003_knowledge_chunk.sql +0 -0
  112. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/004_retrieval_projection.sql +0 -0
  113. {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
  114. {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
  115. {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
  116. {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
  117. {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
  118. {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
  119. {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
  120. {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
  121. {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
  122. {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
  123. {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
  124. {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
  125. {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
  126. {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
  127. {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
  128. {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
  129. {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
  130. {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
  131. {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
  132. {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
  133. {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
  134. {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
  135. {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
  136. {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
  137. {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
  138. {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
  139. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_base/sql/033_knowledge_entity.sql +0 -0
  140. {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
  141. {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
  142. {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
  143. {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
  144. {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
  145. {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
  146. {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
  147. {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
  148. {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
  149. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_build/__init__.py +0 -0
  150. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_build/runtime.py +0 -0
  151. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_build/services/__init__.py +0 -0
  152. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_build/services/document_chunking_service.py +0 -0
  153. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_build/services/heading_patterns.py +0 -0
  154. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_common/__init__.py +0 -0
  155. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_common/exceptions.py +0 -0
  156. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_common/kb_path_utils.py +0 -0
  157. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_common/markdown_reference.py +0 -0
  158. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_common/schemas.py +0 -0
  159. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/knowledge_common/text_segmentation.py +0 -0
  160. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/main.py +0 -0
  161. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/__init__.py +0 -0
  162. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/__init__.py +0 -0
  163. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/answer_synthesizer.py +0 -0
  164. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/multi_hop_react.py +0 -0
  165. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/multi_hop_summarizer.py +0 -0
  166. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/query_decomposer.py +0 -0
  167. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/recursion_fallback.py +0 -0
  168. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/single_hop_react.py +0 -0
  169. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/standalone_question_rewriter.py +0 -0
  170. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/agents/subanswer_aggregator.py +0 -0
  171. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/__init__.py +0 -0
  172. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/base_engine.py +0 -0
  173. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/config.py +0 -0
  174. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/context.py +0 -0
  175. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/context_manager.py +0 -0
  176. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/event_filter.py +0 -0
  177. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/exceptions.py +0 -0
  178. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/fallback_messages.py +0 -0
  179. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/messages.py +0 -0
  180. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/middleware/__init__.py +0 -0
  181. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/middleware/tool_call_guard.py +0 -0
  182. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/models.py +0 -0
  183. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/operation_registry.py +0 -0
  184. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/prompt_fragments.py +0 -0
  185. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/reducers.py +0 -0
  186. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/common/state.py +0 -0
  187. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/__init__.py +0 -0
  188. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/__init__.py +0 -0
  189. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/engine.py +0 -0
  190. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/graph.py +0 -0
  191. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/nodes/__init__.py +0 -0
  192. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/nodes/retrieve.py +0 -0
  193. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/state.py +0 -0
  194. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/fast/types.py +0 -0
  195. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/__init__.py +0 -0
  196. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/engine.py +0 -0
  197. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/graph.py +0 -0
  198. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/nodes/__init__.py +0 -0
  199. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/nodes/final_answer.py +0 -0
  200. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/nodes/router.py +0 -0
  201. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/nodes/state_cleanup.py +0 -0
  202. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/state.py +0 -0
  203. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/engines/instant/types.py +0 -0
  204. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/services/__init__.py +0 -0
  205. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/services/checkpointer_factory.py +0 -0
  206. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/services/llm_service.py +0 -0
  207. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/services/opengauss_checkpointer.py +0 -0
  208. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/services/session_history.py +0 -0
  209. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/tools/__init__.py +0 -0
  210. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/tools/dsl_guide.py +0 -0
  211. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/tools/knowledge_tools.py +0 -0
  212. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/tools/operations/__init__.py +0 -0
  213. {by_qa-0.4.4.dev3 → by_qa-0.4.4.dev4}/src/by_qa/qa/tools/operations/base.py +0 -0
  214. {by_qa-0.4.4.dev3 → 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.dev3
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.dev3"
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
 
@@ -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
- try:
760
- payload = await file_content.read() if file_content is not None else None
761
- request = KnowledgeItemUploadRequest.model_validate(
762
- {
763
- "knCode": kn_code,
764
- "filePath": file_path,
765
- "fileDescription": file_description,
766
- "fileContent": payload,
767
- "mimeType": (
768
- 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)}]}
769
808
  ),
770
- "processFrontMatter": process_front_matter,
771
- "extractZip": extract_zip,
772
- "skipIfDuplicate": skip_if_duplicate,
773
- "metadata": _parse_optional_metadata_form(metadata),
774
- }
775
- )
776
- except (ValidationError, ValueError) as exc:
777
- return _documented_error_response(
778
- result_msg="request validation failed",
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
- logger.info(
787
- "upload_file request received: kb_code=%s, file_path=%s, has_description=%s, process_front_matter=%s, extract_zip=%s",
788
- request.kb_code,
789
- request.file_path,
790
- request.file_description is not None,
791
- request.process_front_matter,
792
- request.extract_zip,
793
- )
794
- filename = (file_content.filename or "") if file_content is not None else ""
795
- try:
796
- service = await get_knowledge_item_ingestion_service()
797
- if request.extract_zip and filename.lower().endswith(".zip"):
798
- 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):
799
878
  return _documented_error_response(
800
- result_msg="invalid zip file",
801
- result_object={},
802
- status_code=422,
879
+ result_msg="unsafe path", result_object={}, status_code=422
803
880
  )
804
- directory_service = await get_knowledge_base_service()
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": [item.model_dump(by_alias=True) for item in result.data],
820
- "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},
821
891
  }
822
- if result.post_process_errors:
823
- result_object["postProcessErrors"] = result.post_process_errors
824
- if result.summary.succeeded > 0:
825
- await _try_schedule_event(
826
- background_tasks,
827
- lambda: build_resource_event(
828
- event_type=ResourceEventType.FILE_IMPORTED,
829
- kb_code=request.kb_code,
830
- source_path=None,
831
- target_path=_ensure_leading_slash(request.file_path),
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
- # single file
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="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,
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
- 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:
@@ -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(self, request: KnowledgeItemUploadRequest) -> dict[str, Any]:
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
- len(request.file_content),
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 = request.file_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=len(content),
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(request.file_content)
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 = hashlib.sha256(file_content).hexdigest()
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,