groundx 3.8.2__tar.gz → 3.8.4__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 (196) hide show
  1. {groundx-3.8.2 → groundx-3.8.4}/PKG-INFO +1 -1
  2. {groundx-3.8.2 → groundx-3.8.4}/pyproject.toml +1 -1
  3. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/client_wrapper.py +2 -2
  4. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/classes/document.py +11 -9
  5. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/classes/groundx.py +8 -4
  6. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/custom_outputs.py +235 -5
  7. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/prompt/utility.py +57 -1
  8. groundx-3.8.4/src/groundx/extract/services/http.py +217 -0
  9. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/services/logger.py +8 -7
  10. {groundx-3.8.2 → groundx-3.8.4}/LICENSE +0 -0
  11. {groundx-3.8.2 → groundx-3.8.4}/README.md +0 -0
  12. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/__init__.py +0 -0
  13. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/_default_clients.py +0 -0
  14. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/api_key/__init__.py +0 -0
  15. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/api_key/client.py +0 -0
  16. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/api_key/raw_client.py +0 -0
  17. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/buckets/__init__.py +0 -0
  18. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/buckets/client.py +0 -0
  19. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/buckets/raw_client.py +0 -0
  20. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/client.py +0 -0
  21. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/__init__.py +0 -0
  22. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/api_error.py +0 -0
  23. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/datetime_utils.py +0 -0
  24. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/file.py +0 -0
  25. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/force_multipart.py +0 -0
  26. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/http_client.py +0 -0
  27. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/http_response.py +0 -0
  28. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/http_sse/__init__.py +0 -0
  29. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/http_sse/_api.py +0 -0
  30. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/http_sse/_decoders.py +0 -0
  31. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/http_sse/_exceptions.py +0 -0
  32. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/http_sse/_models.py +0 -0
  33. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/jsonable_encoder.py +0 -0
  34. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/logging.py +0 -0
  35. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/parse_error.py +0 -0
  36. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/pydantic_utilities.py +0 -0
  37. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/query_encoder.py +0 -0
  38. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/remove_none_from_dict.py +0 -0
  39. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/request_options.py +0 -0
  40. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/core/serialization.py +0 -0
  41. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/csv_splitter.py +0 -0
  42. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/customer/__init__.py +0 -0
  43. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/customer/client.py +0 -0
  44. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/customer/raw_client.py +0 -0
  45. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/documents/__init__.py +0 -0
  46. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/documents/client.py +0 -0
  47. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/documents/raw_client.py +0 -0
  48. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/environment.py +0 -0
  49. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/errors/__init__.py +0 -0
  50. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/errors/bad_request_error.py +0 -0
  51. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/errors/unauthorized_error.py +0 -0
  52. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/__init__.py +0 -0
  53. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/agents/__init__.py +0 -0
  54. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/agents/agent.py +0 -0
  55. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/classes/__init__.py +0 -0
  56. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/classes/agent.py +0 -0
  57. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/classes/api.py +0 -0
  58. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/classes/element.py +0 -0
  59. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/classes/field.py +0 -0
  60. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/classes/group.py +0 -0
  61. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/classes/prompt.py +0 -0
  62. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/classes/testing.py +0 -0
  63. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/post_process/__init__.py +0 -0
  64. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/post_process/post_process.py +0 -0
  65. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/prompt/__init__.py +0 -0
  66. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/prompt/manager.py +0 -0
  67. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/prompt/object_store.py +0 -0
  68. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/prompt/source.py +0 -0
  69. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/services/.DS_Store +0 -0
  70. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/services/__init__.py +0 -0
  71. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/services/csv.py +0 -0
  72. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/services/logging_cfg.py +0 -0
  73. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/services/ratelimit.py +0 -0
  74. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/services/sheets_client.py +0 -0
  75. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/services/status.py +0 -0
  76. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/services/upload.py +0 -0
  77. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/services/upload_minio.py +0 -0
  78. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/services/upload_s3.py +0 -0
  79. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/services/utility.py +0 -0
  80. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/settings/__init__.py +0 -0
  81. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/settings/settings.py +0 -0
  82. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/tasks/__init__.py +0 -0
  83. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/tasks/utility.py +0 -0
  84. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/utility/__init__.py +0 -0
  85. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/utility/utility.py +0 -0
  86. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/extract/workflows.py +0 -0
  87. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/groups/__init__.py +0 -0
  88. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/groups/client.py +0 -0
  89. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/groups/raw_client.py +0 -0
  90. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/health/__init__.py +0 -0
  91. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/health/client.py +0 -0
  92. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/health/raw_client.py +0 -0
  93. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/ingest.py +0 -0
  94. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/py.typed +0 -0
  95. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/search/__init__.py +0 -0
  96. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/search/client.py +0 -0
  97. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/search/raw_client.py +0 -0
  98. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/search/types/__init__.py +0 -0
  99. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/search/types/search_content_request_id.py +0 -0
  100. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/__init__.py +0 -0
  101. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/api_key_detail.py +0 -0
  102. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/api_key_request.py +0 -0
  103. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/api_key_response.py +0 -0
  104. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/bounding_box_detail.py +0 -0
  105. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/bucket_detail.py +0 -0
  106. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/bucket_list_response.py +0 -0
  107. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/bucket_response.py +0 -0
  108. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/bucket_update_detail.py +0 -0
  109. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/bucket_update_response.py +0 -0
  110. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/custom_workflow_leaf_field.py +0 -0
  111. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/custom_workflow_leaf_field_level.py +0 -0
  112. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/custom_workflow_leaf_field_repetition_scope.py +0 -0
  113. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/custom_workflow_output_map.py +0 -0
  114. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/custom_workflow_output_route.py +0 -0
  115. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/custom_workflow_output_route_level.py +0 -0
  116. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/custom_workflow_output_route_output_map.py +0 -0
  117. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/custom_workflow_step.py +0 -0
  118. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/custom_workflow_step_config.py +0 -0
  119. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/custom_workflow_step_element_config.py +0 -0
  120. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/custom_workflow_step_kind.py +0 -0
  121. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/custom_workflow_step_level.py +0 -0
  122. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/customer_detail.py +0 -0
  123. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/customer_detail_status.py +0 -0
  124. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/customer_response.py +0 -0
  125. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/document.py +0 -0
  126. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/document_detail.py +0 -0
  127. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/document_list_response.py +0 -0
  128. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/document_local_ingest_request.py +0 -0
  129. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/document_lookup_response.py +0 -0
  130. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/document_response.py +0 -0
  131. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/document_type.py +0 -0
  132. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/document_update.py +0 -0
  133. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/document_xray.py +0 -0
  134. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/document_xray_chunk.py +0 -0
  135. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/document_xray_page.py +0 -0
  136. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/group_detail.py +0 -0
  137. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/group_list_response.py +0 -0
  138. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/group_response.py +0 -0
  139. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/health_response.py +0 -0
  140. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/health_response_health.py +0 -0
  141. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/health_service.py +0 -0
  142. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/health_service_status.py +0 -0
  143. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/ingest_local_document.py +0 -0
  144. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/ingest_local_document_metadata.py +0 -0
  145. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/ingest_remote_document.py +0 -0
  146. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/ingest_response.py +0 -0
  147. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/ingest_status.py +0 -0
  148. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/ingest_status_light.py +0 -0
  149. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/ingest_status_progress.py +0 -0
  150. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/ingest_status_progress_cancelled.py +0 -0
  151. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/ingest_status_progress_complete.py +0 -0
  152. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/ingest_status_progress_errors.py +0 -0
  153. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/ingest_status_progress_processing.py +0 -0
  154. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/ingest_status_progress_queued.py +0 -0
  155. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/message_response.py +0 -0
  156. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/meter_detail.py +0 -0
  157. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/process_level.py +0 -0
  158. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/processes_status_response.py +0 -0
  159. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/processing_status.py +0 -0
  160. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/search_response.py +0 -0
  161. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/search_response_search.py +0 -0
  162. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/search_result_item.py +0 -0
  163. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/search_result_item_pages_item.py +0 -0
  164. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/sort.py +0 -0
  165. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/sort_order.py +0 -0
  166. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/subscription_detail.py +0 -0
  167. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/subscription_detail_meters.py +0 -0
  168. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/website_source.py +0 -0
  169. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_apply_request.py +0 -0
  170. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_detail.py +0 -0
  171. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_detail_chunk_strategy.py +0 -0
  172. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_detail_relationships.py +0 -0
  173. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_detail_section_strategy.py +0 -0
  174. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_engine.py +0 -0
  175. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_engine_reasoning_effort.py +0 -0
  176. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_engine_service.py +0 -0
  177. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_prompt.py +0 -0
  178. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_prompt_group.py +0 -0
  179. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_prompt_role.py +0 -0
  180. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_request.py +0 -0
  181. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_request_chunk_strategy.py +0 -0
  182. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_request_section_strategy.py +0 -0
  183. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_response.py +0 -0
  184. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_step.py +0 -0
  185. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_step_config.py +0 -0
  186. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_step_config_field.py +0 -0
  187. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_steps.py +0 -0
  188. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflow_template.py +0 -0
  189. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/types/workflows_response.py +0 -0
  190. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/version.py +0 -0
  191. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/workflows/__init__.py +0 -0
  192. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/workflows/client.py +0 -0
  193. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/workflows/raw_client.py +0 -0
  194. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/workflows/types/__init__.py +0 -0
  195. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/workflows/types/workflows_get_request_format.py +0 -0
  196. {groundx-3.8.2 → groundx-3.8.4}/src/groundx/workflows/types/workflows_get_request_id.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: groundx
3
- Version: 3.8.2
3
+ Version: 3.8.4
4
4
  Summary:
5
5
  License: MIT
6
6
  Requires-Python: >=3.10,<4.0
@@ -4,7 +4,7 @@ dynamic = ["version"]
4
4
 
5
5
  [tool.poetry]
6
6
  name = "groundx"
7
- version = "3.8.2"
7
+ version = "3.8.4"
8
8
  description = ""
9
9
  readme = "README.md"
10
10
  authors = []
@@ -27,12 +27,12 @@ class BaseClientWrapper:
27
27
  import platform
28
28
 
29
29
  headers: typing.Dict[str, str] = {
30
- "User-Agent": "groundx/3.8.2",
30
+ "User-Agent": "groundx/3.8.4",
31
31
  "X-Fern-Language": "Python",
32
32
  "X-Fern-Runtime": f"python/{platform.python_version()}",
33
33
  "X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}",
34
34
  "X-Fern-SDK-Name": "groundx",
35
- "X-Fern-SDK-Version": "3.8.2",
35
+ "X-Fern-SDK-Version": "3.8.4",
36
36
  **(self.get_custom_headers() or {}),
37
37
  }
38
38
  headers["X-API-Key"] = self.api_key
@@ -1,20 +1,19 @@
1
1
  import json
2
2
  import os
3
3
  import shutil
4
- import time
5
4
  import typing
6
5
  from datetime import datetime, timezone
7
6
  from io import BytesIO
8
7
  from pathlib import Path
9
8
  from urllib.parse import urlparse
10
9
 
11
- import requests
12
10
  from ..custom_outputs import (
13
11
  custom_output_payload_identity,
14
12
  custom_output_route_values,
15
13
  custom_output_section_identity,
16
14
  )
17
15
  from ..prompt.manager import PromptManager
16
+ from ..services.http import BoundedRequestTimeout, bounded_get
18
17
  from ..services.logger import Logger
19
18
  from ..services.upload import Upload
20
19
  from ..utility import clean_json
@@ -976,23 +975,26 @@ class DocumentRequest(BaseModel):
976
975
 
977
976
  try:
978
977
  self.print("WARN", f"[{attempt}] downloading [{page}]")
979
- resp = requests.get(page)
978
+ resp = bounded_get(
979
+ page,
980
+ operation="page image",
981
+ sleep_between_attempts=should_sleep,
982
+ )
980
983
  resp.raise_for_status()
981
984
  img = Image.open(BytesIO(resp.content))
982
985
  if img:
983
986
  self.page_image_dict[page] = len(self.page_images)
984
987
  self.page_images.append(img)
985
988
  pageImages.append(img)
989
+ except BoundedRequestTimeout as e:
990
+ self.print(
991
+ "ERROR", f"[{attempt}] Failed to load image from {page}: {e}"
992
+ )
993
+ raise RuntimeError(str(e)) from e
986
994
  except Exception as e:
987
995
  self.print(
988
996
  "ERROR", f"[{attempt}] Failed to load image from {page}: {e}"
989
997
  )
990
- if attempt < 2:
991
- if should_sleep:
992
- time.sleep(2 * attempt + 1)
993
- return self.load_images(
994
- imgs, upload, attempt + 1, should_sleep=should_sleep
995
- )
996
998
 
997
999
  return pageImages
998
1000
 
@@ -1,11 +1,13 @@
1
- import json, requests, typing
1
+ import json
2
+ import typing
2
3
  from pathlib import Path
3
4
 
5
+ import requests
6
+ from ..services.http import bounded_get
7
+ from ..services.upload import Upload
4
8
  from pydantic import BaseModel, ConfigDict, Field, field_validator
5
9
  from typing_extensions import Annotated
6
10
 
7
- from ..services.upload import Upload
8
-
9
11
 
10
12
  class GroundXDocument(BaseModel):
11
13
  model_config = ConfigDict(populate_by_name=True)
@@ -195,10 +197,12 @@ class XRayDocument(BaseModel):
195
197
 
196
198
  url = gx_doc.xray_url(base=base)
197
199
  try:
198
- resp = requests.get(url)
200
+ resp = bounded_get(url, operation="X-ray JSON")
199
201
  resp.raise_for_status()
200
202
  except requests.RequestException as e:
201
203
  raise RuntimeError(f"Error fetching X-ray JSON from {url}: {e}")
204
+ except RuntimeError as e:
205
+ raise RuntimeError(f"Error fetching X-ray JSON from {url}: {e}") from e
202
206
 
203
207
  try:
204
208
  payload = resp.json()
@@ -58,8 +58,26 @@ class _RouteContainer:
58
58
  page_numbers: typing.Tuple[int, ...] = ()
59
59
 
60
60
 
61
+ @dataclasses.dataclass(frozen=True)
62
+ class _ScalarCandidate:
63
+ value: typing.Any
64
+ quality: typing.Tuple[int, int, float]
65
+
66
+
61
67
  _REPEATED_STEP_KINDS = {"keys", "summary"}
62
68
  _EXTRACTED_FIELD_VALUE_KEYS = {"value", "confidence", "conflicts", "qa"}
69
+ _DEFAULT_CANDIDATE_VALUES = {
70
+ "n/a",
71
+ "na",
72
+ "none",
73
+ "not applicable",
74
+ "not found",
75
+ "not provided",
76
+ "not specified",
77
+ "not stated",
78
+ "null",
79
+ "unknown",
80
+ }
63
81
  _GET_ALIASES = {
64
82
  "chunkId": ("chunk_id",),
65
83
  "customChunkOutputs": ("custom_chunk_outputs",),
@@ -94,10 +112,12 @@ def reassemble_custom_outputs_from_xray(
94
112
  typing.Tuple[typing.Tuple[str, ...], typing.Tuple[typing.Any, ...]],
95
113
  typing.Dict[str, typing.Any],
96
114
  ] = {}
115
+ scalar_candidates: typing.Dict[str, _ScalarCandidate] = {}
97
116
  workflow_repeated_records: typing.Dict[
98
117
  typing.Tuple[str, typing.Tuple[typing.Any, ...]],
99
118
  typing.Dict[str, typing.Any],
100
119
  ] = {}
120
+ diagnostics: typing.List[CustomOutputDiagnostic] = []
101
121
 
102
122
  for route_index, route in enumerate(routes):
103
123
  route_hits[route_index] = False
@@ -143,14 +163,20 @@ def reassemble_custom_outputs_from_xray(
143
163
  copy.deepcopy(route_value.value),
144
164
  repeated_records=repeated_records,
145
165
  record_key=record_key,
166
+ route=route_map,
167
+ page_numbers=route_container.page_numbers,
168
+ scalar_candidates=scalar_candidates,
169
+ diagnostics=diagnostics,
146
170
  )
147
171
 
148
172
  relationships = workflow.get("output_relationships") if workflow else None
149
173
  relationship_output = None
150
- diagnostics = _missing_required_route_diagnostics(
151
- typing.cast(typing.Sequence[typing.Any], routes),
152
- route_hits,
153
- workflow_extract,
174
+ diagnostics.extend(
175
+ _missing_required_route_diagnostics(
176
+ typing.cast(typing.Sequence[typing.Any], routes),
177
+ route_hits,
178
+ workflow_extract,
179
+ )
154
180
  )
155
181
  if isinstance(relationships, list) and relationships:
156
182
  relationship_output, diagnostics = _apply_relationships(
@@ -159,6 +185,13 @@ def reassemble_custom_outputs_from_xray(
159
185
  diagnostics=diagnostics,
160
186
  )
161
187
  final_output = copy.deepcopy(relationship_output)
188
+ else:
189
+ diagnostics.extend(
190
+ _missing_relationship_metadata_diagnostics(
191
+ final_output,
192
+ workflow_extract,
193
+ )
194
+ )
162
195
 
163
196
  return CustomOutputReassemblyResult(
164
197
  final_output=final_output,
@@ -480,6 +513,56 @@ def _is_empty_output(value: typing.Any) -> bool:
480
513
  return False
481
514
 
482
515
 
516
+ def _scalar_candidate(
517
+ value: typing.Any,
518
+ page_numbers: typing.Tuple[int, ...],
519
+ ) -> _ScalarCandidate:
520
+ return _ScalarCandidate(
521
+ value=value,
522
+ quality=_scalar_candidate_quality(value, page_numbers),
523
+ )
524
+
525
+
526
+ def _scalar_candidate_quality(
527
+ value: typing.Any,
528
+ page_numbers: typing.Tuple[int, ...],
529
+ ) -> typing.Tuple[int, int, float]:
530
+ normalized = _normalized_candidate_value(value)
531
+ is_specific = 0 if _is_default_candidate_value(normalized) else 1
532
+ has_source_pages = 1 if page_numbers else 0
533
+ return (is_specific, has_source_pages, _candidate_confidence(value))
534
+
535
+
536
+ def _normalized_candidate_value(value: typing.Any) -> typing.Any:
537
+ unwrapped = _unwrap_match_value(value)
538
+ if isinstance(unwrapped, str):
539
+ return " ".join(unwrapped.strip().casefold().split())
540
+ if isinstance(unwrapped, numbers.Real):
541
+ return float(unwrapped)
542
+ return unwrapped
543
+
544
+
545
+ def _is_default_candidate_value(value: typing.Any) -> bool:
546
+ if value is None:
547
+ return True
548
+ if isinstance(value, str):
549
+ return value in _DEFAULT_CANDIDATE_VALUES
550
+ return False
551
+
552
+
553
+ def _candidate_confidence(value: typing.Any) -> float:
554
+ if not isinstance(value, typing.Mapping):
555
+ return 0.0
556
+ confidence = value.get("confidence")
557
+ if not isinstance(confidence, numbers.Real):
558
+ return 0.0
559
+ return max(0.0, min(1.0, float(confidence)))
560
+
561
+
562
+ def _string_value(value: typing.Any) -> typing.Optional[str]:
563
+ return value if isinstance(value, str) else None
564
+
565
+
483
566
  def _effective_pointer(pointer: str, *, repeated: bool) -> str:
484
567
  if not repeated or "*" in pointer:
485
568
  return pointer
@@ -499,6 +582,10 @@ def _set_pointer(
499
582
  typing.Dict[str, typing.Any],
500
583
  ],
501
584
  record_key: typing.Tuple[typing.Any, ...],
585
+ route: typing.Mapping[str, typing.Any],
586
+ page_numbers: typing.Tuple[int, ...],
587
+ scalar_candidates: typing.Dict[str, _ScalarCandidate],
588
+ diagnostics: typing.List[CustomOutputDiagnostic],
502
589
  ) -> None:
503
590
  parts = _pointer_parts(pointer)
504
591
  if not parts:
@@ -538,7 +625,28 @@ def _set_pointer(
538
625
  if not isinstance(next_value, dict):
539
626
  return
540
627
  current = next_value
628
+ candidate = _scalar_candidate(value, page_numbers)
629
+ existing = scalar_candidates.get(pointer)
630
+ if existing is not None:
631
+ if candidate.quality < existing.quality:
632
+ return
633
+ if candidate.quality == existing.quality:
634
+ if _normalized_candidate_value(candidate.value) != _normalized_candidate_value(
635
+ existing.value
636
+ ):
637
+ diagnostics.append(
638
+ CustomOutputDiagnostic(
639
+ code="conflicting_output_candidates",
640
+ message=f"multiple equal-quality candidates for [{pointer}]",
641
+ severity="warning",
642
+ workflow_group=_string_value(route.get("workflow_group")),
643
+ workflow_field=_string_value(route.get("workflow_field")),
644
+ final_path=pointer,
645
+ )
646
+ )
647
+ return
541
648
  current[parts[-1]] = value
649
+ scalar_candidates[pointer] = candidate
542
650
 
543
651
 
544
652
  def _set_workflow_value(
@@ -670,6 +778,80 @@ def _missing_required_route_diagnostics(
670
778
  return diagnostics
671
779
 
672
780
 
781
+ def _missing_relationship_metadata_diagnostics(
782
+ final_output: typing.Mapping[str, typing.Any],
783
+ workflow_extract: typing.Optional[typing.Mapping[str, typing.Any]],
784
+ ) -> typing.List[CustomOutputDiagnostic]:
785
+ if not isinstance(workflow_extract, typing.Mapping):
786
+ return []
787
+
788
+ diagnostics: typing.List[CustomOutputDiagnostic] = []
789
+ for group_name, group_spec in _relationship_intent_group_specs(workflow_extract):
790
+ if not isinstance(group_spec, typing.Mapping):
791
+ continue
792
+ if not _group_requires_relationship_metadata(group_spec):
793
+ continue
794
+ group_output = final_output.get(group_name)
795
+ if not isinstance(group_output, list) or len(group_output) == 0:
796
+ continue
797
+ diagnostics.append(
798
+ CustomOutputDiagnostic(
799
+ code="missing_output_relationships",
800
+ message=(
801
+ f"workflow group [{group_name}] requires output relationship "
802
+ "metadata but no output_relationships were provided"
803
+ ),
804
+ workflow_group=group_name,
805
+ )
806
+ )
807
+ return diagnostics
808
+
809
+
810
+ def _relationship_intent_group_specs(
811
+ workflow_extract: typing.Mapping[str, typing.Any],
812
+ ) -> typing.Iterator[typing.Tuple[str, typing.Any]]:
813
+ seen: typing.Set[str] = set()
814
+ for container_key in (
815
+ "_groundx_persisted_extract",
816
+ "groups",
817
+ "prepared_final_groups",
818
+ ):
819
+ container = workflow_extract.get(container_key)
820
+ if not isinstance(container, typing.Mapping):
821
+ continue
822
+ for group_name, group_spec in container.items():
823
+ if isinstance(group_name, str) and group_name not in seen:
824
+ seen.add(group_name)
825
+ yield group_name, group_spec
826
+
827
+ for group_name, group_spec in workflow_extract.items():
828
+ if group_name in {
829
+ "_groundx_persisted_extract",
830
+ "groups",
831
+ "prepared_final_groups",
832
+ "workflow",
833
+ }:
834
+ continue
835
+ if isinstance(group_name, str) and group_name not in seen:
836
+ seen.add(group_name)
837
+ yield group_name, group_spec
838
+
839
+
840
+ def _group_requires_relationship_metadata(
841
+ group_spec: typing.Mapping[str, typing.Any],
842
+ ) -> bool:
843
+ match_attrs = group_spec.get("match_attrs")
844
+ if isinstance(match_attrs, list) and len(match_attrs) > 0:
845
+ return True
846
+ passthrough = group_spec.get("passthrough")
847
+ if isinstance(passthrough, typing.Mapping) and isinstance(
848
+ passthrough.get("from"),
849
+ str,
850
+ ):
851
+ return True
852
+ return False
853
+
854
+
673
855
  def _route_is_required(
674
856
  route: typing.Mapping[str, typing.Any],
675
857
  workflow_extract: typing.Mapping[str, typing.Any],
@@ -834,7 +1016,11 @@ def _apply_relationships(
834
1016
  )
835
1017
  continue
836
1018
 
837
- parent_list = typing.cast(typing.List[typing.Dict[str, typing.Any]], parent_records)
1019
+ parent_list = _dedupe_relationship_parents(
1020
+ typing.cast(typing.List[typing.Dict[str, typing.Any]], parent_records),
1021
+ typing.cast(typing.List[str], match_attrs),
1022
+ )
1023
+ result[parent_group] = parent_list
838
1024
  child_list = typing.cast(typing.List[typing.Dict[str, typing.Any]], child_records)
839
1025
  for parent in parent_list:
840
1026
  parent.setdefault(parent_output_field, [])
@@ -883,6 +1069,50 @@ def _apply_relationships(
883
1069
  return result, diagnostics
884
1070
 
885
1071
 
1072
+ def _dedupe_relationship_parents(
1073
+ parent_list: typing.List[typing.Dict[str, typing.Any]],
1074
+ match_attrs: typing.Sequence[str],
1075
+ ) -> typing.List[typing.Dict[str, typing.Any]]:
1076
+ deduped: typing.List[typing.Dict[str, typing.Any]] = []
1077
+ by_key: typing.Dict[
1078
+ typing.Tuple[typing.Tuple[str, typing.Any], ...],
1079
+ typing.Dict[str, typing.Any],
1080
+ ] = {}
1081
+ for parent in parent_list:
1082
+ parent_key = _match_key(parent, match_attrs)
1083
+ if not parent_key:
1084
+ deduped.append(parent)
1085
+ continue
1086
+ existing = by_key.get(parent_key)
1087
+ if existing is None:
1088
+ by_key[parent_key] = parent
1089
+ deduped.append(parent)
1090
+ continue
1091
+ _merge_relationship_parent(existing, parent)
1092
+ return deduped
1093
+
1094
+
1095
+ def _merge_relationship_parent(
1096
+ target: typing.Dict[str, typing.Any],
1097
+ source: typing.Mapping[str, typing.Any],
1098
+ ) -> None:
1099
+ for key, value in source.items():
1100
+ if key not in target or _match_value_absent(target.get(key)):
1101
+ target[key] = copy.deepcopy(value)
1102
+ continue
1103
+ target_value = target.get(key)
1104
+ if isinstance(target_value, dict) and isinstance(value, typing.Mapping):
1105
+ _merge_relationship_parent(
1106
+ target_value,
1107
+ typing.cast(typing.Mapping[str, typing.Any], value),
1108
+ )
1109
+ continue
1110
+ if isinstance(target_value, list) and isinstance(value, list):
1111
+ for item in value:
1112
+ if item not in target_value:
1113
+ target_value.append(copy.deepcopy(item))
1114
+
1115
+
886
1116
  def _relationship_application_order(
887
1117
  relationships: typing.Sequence[typing.Any],
888
1118
  ) -> typing.List[typing.Any]:
@@ -1628,6 +1628,7 @@ def _collect_custom_workflow_routes(
1628
1628
  step_name: typing.Optional[str],
1629
1629
  steps_by_name: typing.Dict[str, typing.Dict[str, typing.Any]],
1630
1630
  prefix: typing.Tuple[str, ...] = (),
1631
+ document_root: bool = False,
1631
1632
  ) -> typing.Tuple[
1632
1633
  typing.List[typing.Dict[str, typing.Any]],
1633
1634
  typing.List[typing.Dict[str, typing.Any]],
@@ -1655,6 +1656,7 @@ def _collect_custom_workflow_routes(
1655
1656
  step_name,
1656
1657
  steps_by_name,
1657
1658
  (*prefix, field_name, "*"),
1659
+ document_root,
1658
1660
  )
1659
1661
  routes.extend(nested_routes)
1660
1662
  leaves.extend(nested_leaves)
@@ -1683,7 +1685,11 @@ def _collect_custom_workflow_routes(
1683
1685
  output_key_raw,
1684
1686
  f"{field_path}.{_CUSTOM_WORKFLOW_FIELD_METADATA_KEY}",
1685
1687
  )
1686
- segments = (group_name, *prefix, field_name)
1688
+ segments = (
1689
+ (*prefix, field_name)
1690
+ if document_root
1691
+ else (group_name, *prefix, field_name)
1692
+ )
1687
1693
  final_path = _encode_pointer(segments)
1688
1694
  level = typing.cast(str, step["level"])
1689
1695
  workflow_field = _custom_workflow_field_name(prefix, field_name)
@@ -1723,6 +1729,7 @@ def _collect_custom_workflow_routes(
1723
1729
  step_name,
1724
1730
  steps_by_name,
1725
1731
  (*prefix, field_name),
1732
+ document_root,
1726
1733
  )
1727
1734
  routes.extend(nested_routes)
1728
1735
  leaves.extend(nested_leaves)
@@ -1733,6 +1740,7 @@ def _collect_custom_workflow_routes(
1733
1740
  step_name,
1734
1741
  steps_by_name,
1735
1742
  (*prefix, field_name),
1743
+ document_root,
1736
1744
  )
1737
1745
  routes.extend(nested_routes)
1738
1746
  leaves.extend(nested_leaves)
@@ -1740,6 +1748,46 @@ def _collect_custom_workflow_routes(
1740
1748
  return routes, leaves
1741
1749
 
1742
1750
 
1751
+ def _agent_chain_group_roles(raw_chain: typing.Any) -> typing.Dict[str, str]:
1752
+ roles: typing.Dict[str, str] = {}
1753
+ if not isinstance(raw_chain, list):
1754
+ return roles
1755
+
1756
+ for raw_stage in raw_chain:
1757
+ if not isinstance(raw_stage, dict) or set(raw_stage.keys()) != {"parallel"}:
1758
+ continue
1759
+ raw_branches = raw_stage["parallel"]
1760
+ if not isinstance(raw_branches, list):
1761
+ continue
1762
+ for raw_branch in raw_branches:
1763
+ if not isinstance(raw_branch, dict):
1764
+ continue
1765
+ group = raw_branch.get("group")
1766
+ chain = raw_branch.get("chain")
1767
+ if not isinstance(group, str) or not isinstance(chain, list):
1768
+ continue
1769
+ suffixes = {
1770
+ _agent_chain_task_suffix(task)
1771
+ for task in chain
1772
+ if isinstance(task, str)
1773
+ and task in _CUSTOM_WORKFLOW_AGENT_CHAIN_SUPPORTED_TASKS
1774
+ }
1775
+ if len(suffixes) == 1:
1776
+ roles[group] = next(iter(suffixes))
1777
+ return roles
1778
+
1779
+
1780
+ def _is_document_root_statement_group(
1781
+ group_metadata: typing.Mapping[str, typing.Any],
1782
+ group_role: typing.Optional[str],
1783
+ ) -> bool:
1784
+ if group_role != "statement":
1785
+ return False
1786
+ return isinstance(group_metadata.get("final_value_aliases"), dict) or isinstance(
1787
+ group_metadata.get("fill_rules"), list
1788
+ )
1789
+
1790
+
1743
1791
  def _collect_pseudo_custom_workflow_routes(
1744
1792
  group_name: str,
1745
1793
  group: typing.Dict[str, typing.Any],
@@ -1830,6 +1878,9 @@ def _build_authored_custom_workflow_metadata(
1830
1878
 
1831
1879
  routes: typing.List[typing.Dict[str, typing.Any]] = []
1832
1880
  leaves: typing.List[typing.Dict[str, typing.Any]] = []
1881
+ agent_chain_group_roles = _agent_chain_group_roles(
1882
+ workflow.get(_CUSTOM_WORKFLOW_AGENT_CHAIN_KEY)
1883
+ )
1833
1884
  for group_name, group in workflow_groups.items():
1834
1885
  group_step = workflow_group_metadata.get(group_name, {}).get(
1835
1886
  _CUSTOM_WORKFLOW_GROUP_METADATA_KEY
@@ -1846,11 +1897,16 @@ def _build_authored_custom_workflow_metadata(
1846
1897
  workflow_field_paths.get(group_name, {}),
1847
1898
  )
1848
1899
  else:
1900
+ document_root = _is_document_root_statement_group(
1901
+ final_group_metadata.get(group_name, {}),
1902
+ agent_chain_group_roles.get(group_name),
1903
+ )
1849
1904
  group_routes, group_leaves = _collect_custom_workflow_routes(
1850
1905
  fields,
1851
1906
  group_name,
1852
1907
  typing.cast(typing.Optional[str], group_step),
1853
1908
  steps_by_name,
1909
+ document_root=document_root,
1854
1910
  )
1855
1911
  routes.extend(group_routes)
1856
1912
  leaves.extend(group_leaves)