nv-ingest 2025.10.22.dev20251022__tar.gz → 2025.11.8.dev20251108__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.
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/PKG-INFO +1 -1
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/api/v2/README.md +44 -18
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/api/v2/ingest.py +248 -6
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/process/dependent_services.py +17 -10
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/process/strategies.py +6 -2
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/process/termination.py +49 -9
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/examples/pipeline_test_harness.py +2 -2
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/sources/message_broker_task_source.py +41 -8
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/util/service/impl/ingest/redis_ingest_service.py +33 -11
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/pipeline/default_libmode_pipeline_impl.py +2 -2
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/pipeline/default_pipeline_impl.py +22 -21
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest.egg-info/PKG-INFO +1 -1
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/LICENSE +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/MANIFEST.in +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/api/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/api/main.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/api/tracing.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/api/v1/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/api/v1/health.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/api/v1/ingest.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/api/v1/metrics.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/api/v2/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/execution/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/execution/helpers.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/execution/options.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/process/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/process/execution.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/process/lifecycle.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/edges/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/edges/async_queue_edge.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/edges/ray_queue_edge.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/edges/threaded_queue_edge.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/examples/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/examples/task_source_harness.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/examples/task_source_sink_harness.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/primitives/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/primitives/dataclasses.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/primitives/pipeline_monitor.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/primitives/pipeline_topology.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/primitives/ray_pipeline.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/primitives/ray_stat_collector.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/extractors/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/extractors/audio_extractor.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/extractors/chart_extractor.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/extractors/docx_extractor.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/extractors/html_extractor.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/extractors/image_extractor.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/extractors/infographic_extractor.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/extractors/pdf_extractor.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/extractors/pptx_extractor.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/extractors/table_extractor.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/injectors/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/injectors/metadata_injector.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/meta/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/meta/ray_actor_edge_base.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/meta/ray_actor_sink_stage_base.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/meta/ray_actor_source_stage_base.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/meta/ray_actor_stage_base.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/mutate/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/mutate/image_dedup.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/mutate/image_filter.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/sinks/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/sinks/default_drain.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/sinks/message_broker_task_sink.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/sources/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/storage/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/storage/image_storage.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/storage/store_embeddings.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/telemetry/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/telemetry/job_counter.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/telemetry/otel_meter.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/telemetry/otel_tracer.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/transforms/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/transforms/image_caption.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/transforms/text_embed.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/transforms/text_splitter.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/utility/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/stages/utility/throughput_monitor.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/util/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/util/env_config.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/util/pipeline/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/util/pipeline/pid_controller.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/util/pipeline/pipeline_runners.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/util/pipeline/tools.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/util/system_tools/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/util/system_tools/memory.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/orchestration/ray/util/system_tools/visualizers.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/schemas/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/schemas/framework_ingest_config_schema.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/schemas/framework_job_counter_schema.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/schemas/framework_message_broker_sink_schema.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/schemas/framework_message_broker_source_schema.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/schemas/framework_message_wrapper_schema.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/schemas/framework_metadata_injector_schema.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/schemas/framework_otel_meter_schema.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/schemas/framework_otel_tracer_schema.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/schemas/framework_processing_job_schema.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/schemas/framework_task_injection_schema.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/schemas/framework_vdb_task_sink_schema.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/util/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/util/flow_control/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/util/flow_control/filter_by_task.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/util/flow_control/udf_intercept.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/util/service/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/util/service/impl/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/util/service/impl/ingest/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/util/service/meta/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/util/service/meta/ingest/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/util/service/meta/ingest/ingest_service_meta.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/util/telemetry/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/framework/util/telemetry/global_stats.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/pipeline/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/pipeline/config/__init__.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/pipeline/config/loaders.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/pipeline/config/replica_resolver.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/pipeline/ingest_pipeline.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/pipeline/pipeline_schema.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/version.py +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest.egg-info/SOURCES.txt +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest.egg-info/dependency_links.txt +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest.egg-info/requires.txt +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest.egg-info/top_level.txt +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/pyproject.toml +0 -0
- {nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/setup.cfg +0 -0
{nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/api/v2/README.md
RENAMED
|
@@ -11,15 +11,6 @@ The V2 API introduces automatic PDF splitting at the REST layer to improve proce
|
|
|
11
11
|
3. **Transparent Aggregation**: Results are automatically aggregated when fetching parent jobs
|
|
12
12
|
4. **Backward Compatible**: PDFs with page counts ≤ `PDF_SPLIT_PAGE_COUNT` behave identical to V1
|
|
13
13
|
|
|
14
|
-
## Tracing & Aggregated Metadata
|
|
15
|
-
|
|
16
|
-
- V2 endpoints open an OpenTelemetry span using the shared `traced_endpoint` decorator. The span name defaults to the function name, or can be overridden when applying the decorator.
|
|
17
|
-
- `submit_job_v2` records the parent span's `trace_id` into each subjob's `tracing_options`, enabling downstream Ray stages (e.g., the message broker sink) to attach chunk-level telemetry consistently.
|
|
18
|
-
- Response headers still return `x-trace-id` derived from the active span context, allowing clients to correlate downstream work.
|
|
19
|
-
- When `/v2/fetch_job/{parent_id}` aggregates completed chunks, it captures any `trace` / `annotations` dictionaries emitted by the sink for each subjob and includes them in the response payload (see "Aggregated response" below).
|
|
20
|
-
|
|
21
|
-
This behaviour matches the V1 tracing model and sets the foundation for adding W3C `traceparent` propagation in future changes.
|
|
22
|
-
|
|
23
14
|
## How It Works
|
|
24
15
|
|
|
25
16
|
1. **Submit**: When a PDF with pages exceeding `PDF_SPLIT_PAGE_COUNT` is submitted to `/v2/submit_job`:
|
|
@@ -36,6 +27,33 @@ This behaviour matches the V1 tracing model and sets the foundation for adding W
|
|
|
36
27
|
- Pending work returns 202 (processing)
|
|
37
28
|
- Failed chunks are noted without failing the entire job; metadata records which chunks failed
|
|
38
29
|
|
|
30
|
+
|
|
31
|
+
## Client Library Features
|
|
32
|
+
|
|
33
|
+
### Accessing Trace Metrics
|
|
34
|
+
|
|
35
|
+
The Python client library provides convenient access to trace metrics via the `return_traces` parameter:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from nv_ingest_client.client import Ingestor
|
|
39
|
+
|
|
40
|
+
ingestor = Ingestor(
|
|
41
|
+
message_client_hostname="localhost",
|
|
42
|
+
message_client_port=7670,
|
|
43
|
+
message_client_kwargs={"api_version": "v2"}
|
|
44
|
+
).files("/path/to/pdfs").extract().embed()
|
|
45
|
+
|
|
46
|
+
# Get results with trace metrics
|
|
47
|
+
results, traces = ingestor.ingest(return_traces=True)
|
|
48
|
+
|
|
49
|
+
# Access timing for first document
|
|
50
|
+
pdf_time = traces[0]["trace::resident_time::pdf_extractor"] / 1e9
|
|
51
|
+
table_time = traces[0]["trace::resident_time::table_extractor"] / 1e9
|
|
52
|
+
print(f"PDF: {pdf_time:.2f}s, Tables: {table_time:.2f}s")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Note:** For split PDFs, `resident_time` represents aggregated compute time across all chunks. For non-split PDFs, it is computed client-side from entry/exit pairs.
|
|
56
|
+
|
|
39
57
|
### Aggregated response
|
|
40
58
|
|
|
41
59
|
The fetch endpoint returns a JSON body shaped like the following:
|
|
@@ -163,15 +181,23 @@ For split PDFs, parent-level metrics are automatically computed for each stage (
|
|
|
163
181
|
- Failed chunk entries remain in `failed_subjobs`; missing chunks indicate the sink did not emit telemetry
|
|
164
182
|
- **To access chunk traces:** Use `metadata.trace_segments[]` - each segment contains the full trace dict for that chunk
|
|
165
183
|
|
|
166
|
-
|
|
184
|
+
### Advanced: Accessing Full Metadata
|
|
167
185
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
186
|
+
For advanced use cases requiring per-chunk trace breakdown or full metadata, use `include_parent_trace_ids`:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
results, traces, parent_trace_ids = ingestor.ingest(
|
|
190
|
+
return_traces=True,
|
|
191
|
+
include_parent_trace_ids=True
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Fetch full parent job metadata (including trace_segments)
|
|
195
|
+
import requests
|
|
196
|
+
response = requests.get(f"http://localhost:7670/v2/fetch_job/{parent_trace_ids[0]}")
|
|
197
|
+
metadata = response.json()["metadata"]
|
|
173
198
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
199
|
+
# Access per-chunk traces
|
|
200
|
+
for segment in metadata["trace_segments"]:
|
|
201
|
+
print(f"Chunk {segment['chunk_index']}: pages {segment['start_page']}-{segment['end_page']}")
|
|
202
|
+
print(f" Traces: {len(segment['trace'])} entries")
|
|
177
203
|
```
|
{nv_ingest-2025.10.22.dev20251022 → nv_ingest-2025.11.8.dev20251108}/nv_ingest/api/v2/ingest.py
RENAMED
|
@@ -12,6 +12,7 @@ import logging
|
|
|
12
12
|
import os
|
|
13
13
|
import time
|
|
14
14
|
import uuid
|
|
15
|
+
import random
|
|
15
16
|
|
|
16
17
|
from fastapi import APIRouter, Request, Response
|
|
17
18
|
from fastapi import HTTPException
|
|
@@ -44,6 +45,42 @@ router = APIRouter()
|
|
|
44
45
|
|
|
45
46
|
DEFAULT_PDF_SPLIT_PAGE_COUNT = 32
|
|
46
47
|
|
|
48
|
+
# Default QoS thresholds (pages). Tunable via environment variables:
|
|
49
|
+
# QOS_MAX_PAGES_MICRO, QOS_MAX_PAGES_SMALL, QOS_MAX_PAGES_MEDIUM
|
|
50
|
+
_QOS_DEFAULTS = {
|
|
51
|
+
"micro": 8,
|
|
52
|
+
"small": 64,
|
|
53
|
+
"medium": 256,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_qos_tier_for_page_count(page_count: int) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Select QoS tier for a document based on its total page count.
|
|
60
|
+
Tiers: 'micro', 'small', 'medium', 'large', 'default'
|
|
61
|
+
Thresholds can be tuned via environment variables:
|
|
62
|
+
- QOS_MAX_PAGES_MICRO (default: 4)
|
|
63
|
+
- QOS_MAX_PAGES_SMALL (default: 16)
|
|
64
|
+
- QOS_MAX_PAGES_MEDIUM (default: 64)
|
|
65
|
+
Anything above MEDIUM is 'large'. Non-positive page_count returns 'default'.
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
micro_max = int(os.getenv("QOS_MAX_PAGES_MICRO", str(_QOS_DEFAULTS["micro"])))
|
|
69
|
+
small_max = int(os.getenv("QOS_MAX_PAGES_SMALL", str(_QOS_DEFAULTS["small"])))
|
|
70
|
+
medium_max = int(os.getenv("QOS_MAX_PAGES_MEDIUM", str(_QOS_DEFAULTS["medium"])))
|
|
71
|
+
except ValueError:
|
|
72
|
+
micro_max, small_max, medium_max = _QOS_DEFAULTS["micro"], _QOS_DEFAULTS["small"], _QOS_DEFAULTS["medium"]
|
|
73
|
+
|
|
74
|
+
if page_count <= 0:
|
|
75
|
+
return "default"
|
|
76
|
+
if page_count <= micro_max:
|
|
77
|
+
return "micro"
|
|
78
|
+
if page_count <= small_max:
|
|
79
|
+
return "small"
|
|
80
|
+
if page_count <= medium_max:
|
|
81
|
+
return "medium"
|
|
82
|
+
return "large"
|
|
83
|
+
|
|
47
84
|
|
|
48
85
|
def get_pdf_split_page_count(client_override: Optional[int] = None) -> int:
|
|
49
86
|
"""
|
|
@@ -432,6 +469,76 @@ def _extract_ray_telemetry(result: Dict[str, Any]) -> Tuple[Optional[Dict[str, A
|
|
|
432
469
|
return trace_dict, annotations_dict
|
|
433
470
|
|
|
434
471
|
|
|
472
|
+
def _normalize_chunk_records(
|
|
473
|
+
records: Optional[List[Any]],
|
|
474
|
+
descriptor: Dict[str, Any],
|
|
475
|
+
parent_metadata: Dict[str, Any],
|
|
476
|
+
) -> List[Any]:
|
|
477
|
+
"""Re-map chunk-local metadata to document-level context for aggregation."""
|
|
478
|
+
|
|
479
|
+
if not isinstance(records, list):
|
|
480
|
+
return []
|
|
481
|
+
|
|
482
|
+
total_pages = parent_metadata.get("total_pages")
|
|
483
|
+
original_source_id = parent_metadata.get("original_source_id")
|
|
484
|
+
original_source_name = parent_metadata.get("original_source_name")
|
|
485
|
+
|
|
486
|
+
start_page = descriptor.get("start_page")
|
|
487
|
+
page_offset = start_page - 1 if isinstance(start_page, int) and start_page > 0 else 0
|
|
488
|
+
|
|
489
|
+
normalized_entries: List[Any] = []
|
|
490
|
+
|
|
491
|
+
for entry in records:
|
|
492
|
+
if not isinstance(entry, dict):
|
|
493
|
+
normalized_entries.append(entry)
|
|
494
|
+
continue
|
|
495
|
+
|
|
496
|
+
normalized_entry = entry.copy()
|
|
497
|
+
original_metadata = entry.get("metadata")
|
|
498
|
+
|
|
499
|
+
if isinstance(original_metadata, dict):
|
|
500
|
+
normalized_metadata = original_metadata.copy()
|
|
501
|
+
normalized_entry["metadata"] = normalized_metadata
|
|
502
|
+
|
|
503
|
+
original_source_meta = original_metadata.get("source_metadata")
|
|
504
|
+
if isinstance(original_source_meta, dict):
|
|
505
|
+
normalized_source_meta = original_source_meta.copy()
|
|
506
|
+
normalized_metadata["source_metadata"] = normalized_source_meta
|
|
507
|
+
|
|
508
|
+
if original_source_id:
|
|
509
|
+
normalized_source_meta["source_id"] = original_source_id
|
|
510
|
+
if original_source_name:
|
|
511
|
+
normalized_source_meta["source_name"] = original_source_name
|
|
512
|
+
|
|
513
|
+
original_content_meta = original_metadata.get("content_metadata")
|
|
514
|
+
if isinstance(original_content_meta, dict):
|
|
515
|
+
normalized_content_meta = original_content_meta.copy()
|
|
516
|
+
normalized_metadata["content_metadata"] = normalized_content_meta
|
|
517
|
+
|
|
518
|
+
page_number = normalized_content_meta.get("page_number")
|
|
519
|
+
if isinstance(page_number, int) and page_number >= 0:
|
|
520
|
+
normalized_content_meta["page_number"] = page_number + page_offset
|
|
521
|
+
|
|
522
|
+
if isinstance(total_pages, int) and isinstance(normalized_content_meta.get("page_count"), int):
|
|
523
|
+
# Ensure optional per-record page count reflects the full document
|
|
524
|
+
normalized_content_meta["page_count"] = total_pages
|
|
525
|
+
|
|
526
|
+
original_hierarchy = original_content_meta.get("hierarchy")
|
|
527
|
+
if isinstance(original_hierarchy, dict):
|
|
528
|
+
normalized_hierarchy = original_hierarchy.copy()
|
|
529
|
+
normalized_content_meta["hierarchy"] = normalized_hierarchy
|
|
530
|
+
|
|
531
|
+
hierarchy_page = normalized_hierarchy.get("page")
|
|
532
|
+
if isinstance(hierarchy_page, int) and hierarchy_page >= 0:
|
|
533
|
+
normalized_hierarchy["page"] = hierarchy_page + page_offset
|
|
534
|
+
if isinstance(total_pages, int):
|
|
535
|
+
normalized_hierarchy["page_count"] = total_pages
|
|
536
|
+
|
|
537
|
+
normalized_entries.append(normalized_entry)
|
|
538
|
+
|
|
539
|
+
return normalized_entries
|
|
540
|
+
|
|
541
|
+
|
|
435
542
|
def _aggregate_parent_traces(chunk_traces: Dict[str, Any]) -> Dict[str, Any]:
|
|
436
543
|
"""
|
|
437
544
|
Aggregate chunk-level traces into parent-level metrics.
|
|
@@ -574,7 +681,8 @@ def _build_aggregated_response(
|
|
|
574
681
|
if result is not None:
|
|
575
682
|
# Add page data to aggregated result
|
|
576
683
|
if "data" in result:
|
|
577
|
-
|
|
684
|
+
normalized_records = _normalize_chunk_records(result.get("data"), descriptor, metadata)
|
|
685
|
+
aggregated_result["data"].extend(normalized_records)
|
|
578
686
|
chunk_entry = dict(descriptor)
|
|
579
687
|
aggregated_result["metadata"]["chunks"].append(chunk_entry)
|
|
580
688
|
|
|
@@ -631,6 +739,51 @@ def _build_aggregated_response(
|
|
|
631
739
|
return aggregated_result
|
|
632
740
|
|
|
633
741
|
|
|
742
|
+
# ---------------------------------------------------------------------------
|
|
743
|
+
# Bursty submission helpers (fairness without long-lived in-flight tasks)
|
|
744
|
+
# ---------------------------------------------------------------------------
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def _get_submit_burst_params() -> Tuple[int, int, int]:
|
|
748
|
+
"""
|
|
749
|
+
Returns (burst_size, pause_ms, jitter_ms) from environment with sane defaults.
|
|
750
|
+
- V2_SUBMIT_BURST_SIZE (default: 16)
|
|
751
|
+
- V2_SUBMIT_BURST_PAUSE_MS (default: 25)
|
|
752
|
+
- V2_SUBMIT_BURST_JITTER_MS (default: 10)
|
|
753
|
+
"""
|
|
754
|
+
burst_size = int(os.getenv("V2_SUBMIT_BURST_SIZE", "16"))
|
|
755
|
+
pause_ms = int(os.getenv("V2_SUBMIT_BURST_PAUSE_MS", "50"))
|
|
756
|
+
jitter_ms = int(os.getenv("V2_SUBMIT_BURST_JITTER_MS", "15"))
|
|
757
|
+
|
|
758
|
+
return max(1, burst_size), max(0, pause_ms), max(0, jitter_ms)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
async def _submit_subjobs_in_bursts(
|
|
762
|
+
items: List[Tuple[str, MessageWrapper]],
|
|
763
|
+
ingest_service: "INGEST_SERVICE_T",
|
|
764
|
+
*,
|
|
765
|
+
burst_size: int,
|
|
766
|
+
pause_ms: int,
|
|
767
|
+
jitter_ms: int,
|
|
768
|
+
) -> None:
|
|
769
|
+
"""
|
|
770
|
+
Submit subjobs in sequential bursts and await each burst to completion.
|
|
771
|
+
This avoids keeping a large number of pending tasks in the REST handler
|
|
772
|
+
and allows other concurrent requests to interleave enqueue work between bursts.
|
|
773
|
+
"""
|
|
774
|
+
for offset in range(0, len(items), burst_size):
|
|
775
|
+
burst = items[offset : offset + burst_size]
|
|
776
|
+
tasks = [ingest_service.submit_job(wrapper, subjob_id) for (subjob_id, wrapper) in burst]
|
|
777
|
+
# Propagate any errors from this burst
|
|
778
|
+
await asyncio.gather(*tasks)
|
|
779
|
+
|
|
780
|
+
# Pause with jitter to yield to other request handlers before next burst
|
|
781
|
+
if offset + burst_size < len(items):
|
|
782
|
+
delay_ms = pause_ms + (random.randint(0, jitter_ms) if jitter_ms > 0 else 0)
|
|
783
|
+
if delay_ms > 0:
|
|
784
|
+
await asyncio.sleep(delay_ms / 1000.0)
|
|
785
|
+
|
|
786
|
+
|
|
634
787
|
# POST /v2/submit_job
|
|
635
788
|
@router.post(
|
|
636
789
|
"/submit_job",
|
|
@@ -672,27 +825,33 @@ async def submit_job_v2(
|
|
|
672
825
|
original_source_id = source_ids[0] if source_ids else "unknown_source.pdf"
|
|
673
826
|
original_source_name = source_names[0] if source_names else "unknown_source.pdf"
|
|
674
827
|
|
|
828
|
+
# Track page count for all PDFs (used for both splitting logic and metadata)
|
|
829
|
+
pdf_page_count_cache = None
|
|
830
|
+
|
|
675
831
|
# Check if this is a PDF that needs splitting
|
|
676
832
|
if document_types and payloads and document_types[0].lower() == "pdf":
|
|
677
833
|
# Decode the payload to check page count
|
|
678
834
|
pdf_content = base64.b64decode(payloads[0])
|
|
679
835
|
page_count = get_pdf_page_count(pdf_content)
|
|
836
|
+
pdf_page_count_cache = page_count # Cache for later use
|
|
837
|
+
qos_tier = get_qos_tier_for_page_count(page_count)
|
|
680
838
|
pages_per_chunk = get_pdf_split_page_count(client_override=client_split_page_count)
|
|
681
839
|
|
|
682
840
|
# Split if the document has more pages than our chunk size
|
|
683
841
|
if page_count > pages_per_chunk:
|
|
684
842
|
logger.warning(
|
|
685
|
-
"Splitting PDF %s into %s-page chunks (total pages: %s)",
|
|
843
|
+
"Splitting PDF %s into %s-page chunks (total pages: %s) -> (qos_tier: %s)",
|
|
686
844
|
original_source_name,
|
|
687
845
|
pages_per_chunk,
|
|
688
846
|
page_count,
|
|
847
|
+
qos_tier,
|
|
689
848
|
)
|
|
690
849
|
|
|
691
850
|
chunks = split_pdf_to_chunks(pdf_content, pages_per_chunk)
|
|
692
851
|
|
|
693
852
|
subjob_ids: List[str] = []
|
|
694
853
|
subjob_descriptors: List[Dict[str, Any]] = []
|
|
695
|
-
|
|
854
|
+
submission_items: List[Tuple[str, MessageWrapper]] = []
|
|
696
855
|
|
|
697
856
|
try:
|
|
698
857
|
parent_uuid = uuid.UUID(parent_job_id)
|
|
@@ -713,7 +872,19 @@ async def submit_job_v2(
|
|
|
713
872
|
original_source_id=original_source_id,
|
|
714
873
|
original_source_name=original_source_name,
|
|
715
874
|
)
|
|
716
|
-
|
|
875
|
+
|
|
876
|
+
# Inject QoS routing hint into subjob routing_options (keeps API and service loosely coupled)
|
|
877
|
+
try:
|
|
878
|
+
sub_spec = json.loads(subjob_wrapper.payload)
|
|
879
|
+
routing_opts = sub_spec.get("routing_options") or {}
|
|
880
|
+
routing_opts["queue_hint"] = qos_tier
|
|
881
|
+
sub_spec["routing_options"] = routing_opts
|
|
882
|
+
subjob_wrapper = MessageWrapper(payload=json.dumps(sub_spec))
|
|
883
|
+
except Exception:
|
|
884
|
+
# Best-effort; if we cannot inject, fall back to default routing
|
|
885
|
+
pass
|
|
886
|
+
|
|
887
|
+
submission_items.append((subjob_id, subjob_wrapper))
|
|
717
888
|
subjob_ids.append(subjob_id)
|
|
718
889
|
subjob_descriptors.append(
|
|
719
890
|
{
|
|
@@ -725,8 +896,15 @@ async def submit_job_v2(
|
|
|
725
896
|
}
|
|
726
897
|
)
|
|
727
898
|
|
|
728
|
-
if
|
|
729
|
-
|
|
899
|
+
if submission_items:
|
|
900
|
+
burst_size, pause_ms, jitter_ms = _get_submit_burst_params()
|
|
901
|
+
await _submit_subjobs_in_bursts(
|
|
902
|
+
submission_items,
|
|
903
|
+
ingest_service,
|
|
904
|
+
burst_size=burst_size,
|
|
905
|
+
pause_ms=pause_ms,
|
|
906
|
+
jitter_ms=jitter_ms,
|
|
907
|
+
)
|
|
730
908
|
|
|
731
909
|
parent_metadata: Dict[str, Any] = {
|
|
732
910
|
"total_pages": page_count,
|
|
@@ -754,6 +932,16 @@ async def submit_job_v2(
|
|
|
754
932
|
if "tracing_options" not in job_spec_dict:
|
|
755
933
|
job_spec_dict["tracing_options"] = {"trace": True}
|
|
756
934
|
job_spec_dict["tracing_options"]["trace_id"] = str(current_trace_id)
|
|
935
|
+
# If this was a PDF and we computed page_count, route the single job using the same QoS tier
|
|
936
|
+
try:
|
|
937
|
+
if (
|
|
938
|
+
document_types
|
|
939
|
+
and document_types[0].lower() == "pdf"
|
|
940
|
+
and "queue_hint" not in (job_spec_dict.get("routing_options") or {})
|
|
941
|
+
):
|
|
942
|
+
job_spec_dict.setdefault("routing_options", {})["queue_hint"] = qos_tier
|
|
943
|
+
except Exception:
|
|
944
|
+
pass
|
|
757
945
|
updated_job_spec = MessageWrapper(payload=json.dumps(job_spec_dict))
|
|
758
946
|
|
|
759
947
|
span.add_event("Submitting as single job (no split needed)")
|
|
@@ -762,6 +950,34 @@ async def submit_job_v2(
|
|
|
762
950
|
await ingest_service.submit_job(updated_job_spec, parent_job_id)
|
|
763
951
|
await ingest_service.set_job_state(parent_job_id, STATE_SUBMITTED)
|
|
764
952
|
|
|
953
|
+
# If this was a PDF (even if not split), store page count metadata for tracking
|
|
954
|
+
if pdf_page_count_cache is not None:
|
|
955
|
+
try:
|
|
956
|
+
# Use cached page count from earlier check to avoid re-decoding
|
|
957
|
+
# Store minimal metadata for non-split PDFs (consistent with split PDFs)
|
|
958
|
+
single_pdf_metadata: Dict[str, Any] = {
|
|
959
|
+
"total_pages": pdf_page_count_cache,
|
|
960
|
+
"pages_per_chunk": pdf_page_count_cache, # Single chunk = entire document
|
|
961
|
+
"original_source_id": original_source_id,
|
|
962
|
+
"original_source_name": original_source_name,
|
|
963
|
+
"document_type": document_types[0],
|
|
964
|
+
"subjob_order": [], # No subjobs for non-split PDFs
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
# Store as parent job metadata with empty subjob list for consistency
|
|
968
|
+
await ingest_service.set_parent_job_mapping(
|
|
969
|
+
parent_job_id,
|
|
970
|
+
[], # Empty subjob list
|
|
971
|
+
single_pdf_metadata,
|
|
972
|
+
subjob_descriptors=[],
|
|
973
|
+
)
|
|
974
|
+
logger.debug(
|
|
975
|
+
f"Stored page count metadata for non-split PDF {original_source_name}: {pdf_page_count_cache} pages"
|
|
976
|
+
)
|
|
977
|
+
except Exception as metadata_err:
|
|
978
|
+
# Don't fail the job if metadata storage fails
|
|
979
|
+
logger.warning(f"Failed to store page count metadata for {parent_job_id}: {metadata_err}")
|
|
980
|
+
|
|
765
981
|
response.headers["x-trace-id"] = trace.format_trace_id(current_trace_id)
|
|
766
982
|
return parent_job_id
|
|
767
983
|
|
|
@@ -898,6 +1114,32 @@ async def fetch_job_v2(job_id: str, ingest_service: INGEST_SERVICE_T):
|
|
|
898
1114
|
|
|
899
1115
|
logger.debug(f"Parent job {job_id} has {len(subjob_ids)} subjobs")
|
|
900
1116
|
|
|
1117
|
+
# Special case: Non-split PDFs have metadata but no subjobs
|
|
1118
|
+
# Fetch the result directly and augment with page count metadata
|
|
1119
|
+
if len(subjob_ids) == 0:
|
|
1120
|
+
logger.debug(f"Job {job_id} is a non-split PDF, fetching result directly")
|
|
1121
|
+
try:
|
|
1122
|
+
job_response = await ingest_service.fetch_job(job_id)
|
|
1123
|
+
|
|
1124
|
+
# Augment response with page count metadata
|
|
1125
|
+
if isinstance(job_response, dict):
|
|
1126
|
+
if "metadata" not in job_response:
|
|
1127
|
+
job_response["metadata"] = {}
|
|
1128
|
+
job_response["metadata"]["total_pages"] = metadata.get("total_pages")
|
|
1129
|
+
job_response["metadata"]["original_source_id"] = metadata.get("original_source_id")
|
|
1130
|
+
job_response["metadata"]["original_source_name"] = metadata.get("original_source_name")
|
|
1131
|
+
|
|
1132
|
+
# Update job state after successful fetch
|
|
1133
|
+
await _update_job_state_after_fetch(job_id, ingest_service)
|
|
1134
|
+
|
|
1135
|
+
return _stream_json_response(job_response)
|
|
1136
|
+
except (TimeoutError, RedisError, ConnectionError):
|
|
1137
|
+
logger.debug(f"Job {job_id} (non-split PDF) not ready yet")
|
|
1138
|
+
raise HTTPException(status_code=202, detail="Job is processing. Retry later.")
|
|
1139
|
+
except Exception as e:
|
|
1140
|
+
logger.exception(f"Error fetching non-split PDF job {job_id}: {e}")
|
|
1141
|
+
raise HTTPException(status_code=500, detail="Internal server error during job fetch.")
|
|
1142
|
+
|
|
901
1143
|
# Build ordered descriptors for subjobs
|
|
902
1144
|
stored_descriptors = subjob_info.get("subjob_descriptors") or []
|
|
903
1145
|
descriptor_lookup = {entry.get("job_id"): entry for entry in stored_descriptors if isinstance(entry, dict)}
|
|
@@ -18,6 +18,18 @@ from nv_ingest_api.util.message_brokers.simple_message_broker.broker import Simp
|
|
|
18
18
|
logger = logging.getLogger(__name__)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def _broker_server_target(host, port, max_queue_size):
|
|
22
|
+
"""
|
|
23
|
+
Target function to be run in a separate process for the SimpleMessageBroker.
|
|
24
|
+
"""
|
|
25
|
+
server = SimpleMessageBroker(host, port, max_queue_size)
|
|
26
|
+
try:
|
|
27
|
+
server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
server.serve_forever()
|
|
31
|
+
|
|
32
|
+
|
|
21
33
|
def start_simple_message_broker(broker_client: dict) -> multiprocessing.Process:
|
|
22
34
|
"""
|
|
23
35
|
Starts a SimpleMessageBroker server in a separate process.
|
|
@@ -58,16 +70,11 @@ def start_simple_message_broker(broker_client: dict) -> multiprocessing.Process:
|
|
|
58
70
|
f"continuing to spawn a broker process (tests expect a Process to be returned)"
|
|
59
71
|
)
|
|
60
72
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
except Exception:
|
|
67
|
-
pass
|
|
68
|
-
server.serve_forever()
|
|
69
|
-
|
|
70
|
-
p = multiprocessing.Process(target=broker_server)
|
|
73
|
+
p = multiprocessing.Process(
|
|
74
|
+
target=_broker_server_target,
|
|
75
|
+
args=(server_host, server_port, max_queue_size),
|
|
76
|
+
daemon=True,
|
|
77
|
+
)
|
|
71
78
|
# If we're launching from inside the pipeline subprocess, mark daemon so the
|
|
72
79
|
# broker dies automatically when the subprocess exits.
|
|
73
80
|
p.daemon = os.environ.get("NV_INGEST_BROKER_IN_SUBPROCESS") == "1"
|
|
@@ -11,9 +11,10 @@ Strategy pattern for clean separation of execution concerns.
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import atexit
|
|
14
|
-
import os
|
|
15
14
|
import logging
|
|
16
15
|
import multiprocessing
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
17
18
|
import time
|
|
18
19
|
from abc import ABC, abstractmethod
|
|
19
20
|
|
|
@@ -132,7 +133,10 @@ class SubprocessStrategy(ProcessExecutionStrategy):
|
|
|
132
133
|
logger.info("Launching pipeline in Python subprocess using multiprocessing.")
|
|
133
134
|
|
|
134
135
|
# Create subprocess using fork context
|
|
135
|
-
|
|
136
|
+
start_method = "fork"
|
|
137
|
+
if sys.platform.lower() == "darwin":
|
|
138
|
+
start_method = "spawn"
|
|
139
|
+
ctx = multiprocessing.get_context(start_method)
|
|
136
140
|
process = ctx.Process(
|
|
137
141
|
target=run_pipeline_process,
|
|
138
142
|
args=(
|
|
@@ -19,20 +19,45 @@ logger = logging.getLogger(__name__)
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def _safe_log(level: int, msg: str) -> None:
|
|
22
|
-
"""Best-effort logging that won't
|
|
22
|
+
"""Best-effort logging that won't emit handler tracebacks on closed streams.
|
|
23
|
+
|
|
24
|
+
Temporarily disables logging.raiseExceptions to prevent the logging module
|
|
25
|
+
from printing "--- Logging error ---" to stderr if a handler's stream is
|
|
26
|
+
already closed (common during process teardown). Falls back to writing to
|
|
27
|
+
sys.__stderr__ if available.
|
|
28
|
+
"""
|
|
23
29
|
try:
|
|
24
|
-
|
|
30
|
+
import logging as _logging
|
|
31
|
+
|
|
32
|
+
prev = getattr(_logging, "raiseExceptions", True)
|
|
33
|
+
# Suppress handler errors being printed to stderr
|
|
34
|
+
_logging.raiseExceptions = False
|
|
35
|
+
|
|
36
|
+
# If there are no handlers, skip and use stderr fallback
|
|
37
|
+
if logger.handlers:
|
|
38
|
+
logger.log(level, msg)
|
|
39
|
+
return
|
|
25
40
|
except Exception:
|
|
41
|
+
# Intentionally ignore and try stderr fallback
|
|
42
|
+
pass
|
|
43
|
+
finally:
|
|
26
44
|
try:
|
|
27
|
-
#
|
|
28
|
-
import sys
|
|
45
|
+
import logging as _logging # re-import safe even if earlier failed
|
|
29
46
|
|
|
30
|
-
|
|
31
|
-
sys.__stderr__.write(msg + "\n")
|
|
32
|
-
sys.__stderr__.flush()
|
|
47
|
+
_logging.raiseExceptions = prev # type: ignore[name-defined]
|
|
33
48
|
except Exception:
|
|
34
49
|
pass
|
|
35
50
|
|
|
51
|
+
# Fallback to stderr if available
|
|
52
|
+
try:
|
|
53
|
+
import sys
|
|
54
|
+
|
|
55
|
+
if hasattr(sys, "__stderr__") and sys.__stderr__:
|
|
56
|
+
sys.__stderr__.write(msg + "\n")
|
|
57
|
+
sys.__stderr__.flush()
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
|
|
36
61
|
|
|
37
62
|
def kill_pipeline_process_group(process) -> None:
|
|
38
63
|
"""
|
|
@@ -74,7 +99,17 @@ def kill_pipeline_process_group(process) -> None:
|
|
|
74
99
|
|
|
75
100
|
try:
|
|
76
101
|
# Send graceful termination to the entire process group
|
|
77
|
-
|
|
102
|
+
try:
|
|
103
|
+
pgid = os.getpgid(pid)
|
|
104
|
+
except Exception:
|
|
105
|
+
# Process already gone
|
|
106
|
+
_safe_log(logging.DEBUG, f"Process group for PID {pid} not found during SIGTERM phase")
|
|
107
|
+
return
|
|
108
|
+
try:
|
|
109
|
+
os.killpg(pgid, signal.SIGTERM)
|
|
110
|
+
except ProcessLookupError:
|
|
111
|
+
_safe_log(logging.DEBUG, f"Process group for PID {pid} no longer exists (SIGTERM)")
|
|
112
|
+
return
|
|
78
113
|
|
|
79
114
|
# If we have a Process handle, give it a chance to exit cleanly
|
|
80
115
|
if proc is not None and hasattr(proc, "join"):
|
|
@@ -95,7 +130,12 @@ def kill_pipeline_process_group(process) -> None:
|
|
|
95
130
|
if still_alive:
|
|
96
131
|
_safe_log(logging.WARNING, "Process group did not terminate gracefully, using SIGKILL")
|
|
97
132
|
try:
|
|
98
|
-
|
|
133
|
+
try:
|
|
134
|
+
pgid2 = os.getpgid(pid)
|
|
135
|
+
except Exception:
|
|
136
|
+
_safe_log(logging.DEBUG, f"Process group for PID {pid} vanished before SIGKILL")
|
|
137
|
+
return
|
|
138
|
+
os.killpg(pgid2, signal.SIGKILL)
|
|
99
139
|
finally:
|
|
100
140
|
if proc is not None and hasattr(proc, "join"):
|
|
101
141
|
try:
|
|
@@ -152,11 +152,11 @@ if __name__ == "__main__":
|
|
|
152
152
|
os.environ["OCR_MODEL_NAME"] = "paddle"
|
|
153
153
|
os.environ["NEMORETRIEVER_PARSE_HTTP_ENDPOINT"] = "https://integrate.api.nvidia.com/v1/chat/completions"
|
|
154
154
|
os.environ["VLM_CAPTION_ENDPOINT"] = "https://integrate.api.nvidia.com/v1/chat/completions"
|
|
155
|
-
os.environ["VLM_CAPTION_MODEL_NAME"] = "nvidia/
|
|
155
|
+
os.environ["VLM_CAPTION_MODEL_NAME"] = "nvidia/nemotron-nano-12b-v2-vl"
|
|
156
156
|
logger.info("Environment variables set.")
|
|
157
157
|
|
|
158
158
|
image_caption_endpoint_url = "https://integrate.api.nvidia.com/v1/chat/completions"
|
|
159
|
-
model_name = "nvidia/
|
|
159
|
+
model_name = "nvidia/nemotron-nano-12b-v2-vl"
|
|
160
160
|
yolox_grpc, yolox_http, yolox_auth, yolox_protocol = get_nim_service("yolox")
|
|
161
161
|
(
|
|
162
162
|
yolox_table_structure_grpc,
|