OpenRAG 0.3.4__tar.gz → 0.4.0__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 (164) hide show
  1. {openrag-0.3.4/src/openrag.egg-info → openrag-0.4.0}/PKG-INFO +2 -1
  2. {openrag-0.3.4 → openrag-0.4.0}/pyproject.toml +3 -2
  3. {openrag-0.3.4 → openrag-0.4.0}/src/agent.py +29 -24
  4. {openrag-0.3.4 → openrag-0.4.0}/src/api/connectors.py +57 -5
  5. {openrag-0.3.4 → openrag-0.4.0}/src/api/documents.py +15 -8
  6. {openrag-0.3.4 → openrag-0.4.0}/src/api/settings.py +150 -61
  7. {openrag-0.3.4 → openrag-0.4.0}/src/api/v1/chat.py +2 -0
  8. {openrag-0.3.4 → openrag-0.4.0}/src/config/settings.py +2 -0
  9. {openrag-0.3.4 → openrag-0.4.0}/src/connectors/__init__.py +4 -0
  10. openrag-0.4.0/src/connectors/aws_s3/__init__.py +19 -0
  11. openrag-0.4.0/src/connectors/aws_s3/api.py +175 -0
  12. openrag-0.4.0/src/connectors/aws_s3/auth.py +90 -0
  13. openrag-0.4.0/src/connectors/aws_s3/connector.py +277 -0
  14. openrag-0.4.0/src/connectors/aws_s3/models.py +13 -0
  15. openrag-0.4.0/src/connectors/aws_s3/support.py +51 -0
  16. {openrag-0.3.4 → openrag-0.4.0}/src/connectors/connection_manager.py +57 -9
  17. openrag-0.4.0/src/connectors/ibm_cos/__init__.py +17 -0
  18. openrag-0.4.0/src/connectors/ibm_cos/api.py +202 -0
  19. openrag-0.4.0/src/connectors/ibm_cos/auth.py +183 -0
  20. openrag-0.4.0/src/connectors/ibm_cos/connector.py +375 -0
  21. openrag-0.4.0/src/connectors/ibm_cos/models.py +20 -0
  22. openrag-0.4.0/src/connectors/ibm_cos/support.py +68 -0
  23. {openrag-0.3.4 → openrag-0.4.0}/src/connectors/langflow_connector_service.py +30 -29
  24. {openrag-0.3.4 → openrag-0.4.0}/src/main.py +75 -56
  25. {openrag-0.3.4 → openrag-0.4.0}/src/models/processors.py +52 -23
  26. {openrag-0.3.4 → openrag-0.4.0/src/openrag.egg-info}/PKG-INFO +2 -1
  27. {openrag-0.3.4 → openrag-0.4.0}/src/openrag.egg-info/SOURCES.txt +12 -0
  28. {openrag-0.3.4 → openrag-0.4.0}/src/openrag.egg-info/requires.txt +1 -0
  29. {openrag-0.3.4 → openrag-0.4.0}/src/services/auth_service.py +69 -4
  30. {openrag-0.3.4 → openrag-0.4.0}/src/services/chat_service.py +10 -9
  31. {openrag-0.3.4 → openrag-0.4.0}/src/services/flows_service.py +110 -36
  32. {openrag-0.3.4 → openrag-0.4.0}/src/services/search_service.py +70 -7
  33. {openrag-0.3.4 → openrag-0.4.0}/src/services/task_service.py +33 -24
  34. {openrag-0.3.4 → openrag-0.4.0}/src/tui/_assets/docker-compose.yml +11 -1
  35. {openrag-0.3.4 → openrag-0.4.0}/src/tui/_assets/flows/ingestion_flow.json +2 -2
  36. {openrag-0.3.4 → openrag-0.4.0}/src/tui/_assets/flows/openrag_agent.json +4 -4
  37. {openrag-0.3.4 → openrag-0.4.0}/src/tui/_assets/flows/openrag_nudges.json +2 -2
  38. {openrag-0.3.4 → openrag-0.4.0}/src/tui/_assets/flows/openrag_url_mcp.json +2 -2
  39. {openrag-0.3.4 → openrag-0.4.0}/src/tui/config_fields.py +47 -0
  40. {openrag-0.3.4 → openrag-0.4.0}/src/tui/main.py +83 -30
  41. {openrag-0.3.4 → openrag-0.4.0}/src/tui/managers/container_manager.py +1 -1
  42. {openrag-0.3.4 → openrag-0.4.0}/src/tui/managers/env_manager.py +31 -0
  43. {openrag-0.3.4 → openrag-0.4.0}/src/tui/screens/config.py +83 -0
  44. {openrag-0.3.4 → openrag-0.4.0}/src/tui/screens/monitor.py +25 -6
  45. {openrag-0.3.4 → openrag-0.4.0}/src/tui/utils/version_check.py +10 -7
  46. {openrag-0.3.4 → openrag-0.4.0}/src/utils/file_utils.py +47 -5
  47. {openrag-0.3.4 → openrag-0.4.0}/src/utils/version_utils.py +6 -1
  48. {openrag-0.3.4 → openrag-0.4.0}/LICENSE +0 -0
  49. {openrag-0.3.4 → openrag-0.4.0}/MANIFEST.in +0 -0
  50. {openrag-0.3.4 → openrag-0.4.0}/README.md +0 -0
  51. {openrag-0.3.4 → openrag-0.4.0}/setup.cfg +0 -0
  52. {openrag-0.3.4 → openrag-0.4.0}/src/api/__init__.py +0 -0
  53. {openrag-0.3.4 → openrag-0.4.0}/src/api/auth.py +0 -0
  54. {openrag-0.3.4 → openrag-0.4.0}/src/api/chat.py +0 -0
  55. {openrag-0.3.4 → openrag-0.4.0}/src/api/connector_router.py +0 -0
  56. {openrag-0.3.4 → openrag-0.4.0}/src/api/docling.py +0 -0
  57. {openrag-0.3.4 → openrag-0.4.0}/src/api/flows.py +0 -0
  58. {openrag-0.3.4 → openrag-0.4.0}/src/api/keys.py +0 -0
  59. {openrag-0.3.4 → openrag-0.4.0}/src/api/knowledge_filter.py +0 -0
  60. {openrag-0.3.4 → openrag-0.4.0}/src/api/langflow_files.py +0 -0
  61. {openrag-0.3.4 → openrag-0.4.0}/src/api/models.py +0 -0
  62. {openrag-0.3.4 → openrag-0.4.0}/src/api/nudges.py +0 -0
  63. {openrag-0.3.4 → openrag-0.4.0}/src/api/oidc.py +0 -0
  64. {openrag-0.3.4 → openrag-0.4.0}/src/api/provider_health.py +0 -0
  65. {openrag-0.3.4 → openrag-0.4.0}/src/api/provider_validation.py +0 -0
  66. {openrag-0.3.4 → openrag-0.4.0}/src/api/router.py +0 -0
  67. {openrag-0.3.4 → openrag-0.4.0}/src/api/search.py +0 -0
  68. {openrag-0.3.4 → openrag-0.4.0}/src/api/tasks.py +0 -0
  69. {openrag-0.3.4 → openrag-0.4.0}/src/api/upload.py +0 -0
  70. {openrag-0.3.4 → openrag-0.4.0}/src/api/v1/__init__.py +0 -0
  71. {openrag-0.3.4 → openrag-0.4.0}/src/api/v1/documents.py +0 -0
  72. {openrag-0.3.4 → openrag-0.4.0}/src/api/v1/knowledge_filters.py +0 -0
  73. {openrag-0.3.4 → openrag-0.4.0}/src/api/v1/models.py +0 -0
  74. {openrag-0.3.4 → openrag-0.4.0}/src/api/v1/search.py +0 -0
  75. {openrag-0.3.4 → openrag-0.4.0}/src/api/v1/settings.py +0 -0
  76. {openrag-0.3.4 → openrag-0.4.0}/src/auth_context.py +0 -0
  77. {openrag-0.3.4 → openrag-0.4.0}/src/config/__init__.py +0 -0
  78. {openrag-0.3.4 → openrag-0.4.0}/src/config/config_manager.py +0 -0
  79. {openrag-0.3.4 → openrag-0.4.0}/src/config/model_constants.py +0 -0
  80. {openrag-0.3.4 → openrag-0.4.0}/src/connectors/base.py +0 -0
  81. {openrag-0.3.4 → openrag-0.4.0}/src/connectors/google_drive/__init__.py +0 -0
  82. {openrag-0.3.4 → openrag-0.4.0}/src/connectors/google_drive/connector.py +0 -0
  83. {openrag-0.3.4 → openrag-0.4.0}/src/connectors/google_drive/oauth.py +0 -0
  84. {openrag-0.3.4 → openrag-0.4.0}/src/connectors/onedrive/__init__.py +0 -0
  85. {openrag-0.3.4 → openrag-0.4.0}/src/connectors/onedrive/connector.py +0 -0
  86. {openrag-0.3.4 → openrag-0.4.0}/src/connectors/onedrive/oauth.py +0 -0
  87. {openrag-0.3.4 → openrag-0.4.0}/src/connectors/service.py +0 -0
  88. {openrag-0.3.4 → openrag-0.4.0}/src/connectors/sharepoint/__init__.py +0 -0
  89. {openrag-0.3.4 → openrag-0.4.0}/src/connectors/sharepoint/connector.py +0 -0
  90. {openrag-0.3.4 → openrag-0.4.0}/src/connectors/sharepoint/oauth.py +0 -0
  91. {openrag-0.3.4 → openrag-0.4.0}/src/connectors/sharepoint/utils.py +0 -0
  92. {openrag-0.3.4 → openrag-0.4.0}/src/dependencies.py +0 -0
  93. {openrag-0.3.4 → openrag-0.4.0}/src/models/__init__.py +0 -0
  94. {openrag-0.3.4 → openrag-0.4.0}/src/models/tasks.py +0 -0
  95. {openrag-0.3.4 → openrag-0.4.0}/src/models/url.py +0 -0
  96. {openrag-0.3.4 → openrag-0.4.0}/src/openrag.egg-info/dependency_links.txt +0 -0
  97. {openrag-0.3.4 → openrag-0.4.0}/src/openrag.egg-info/entry_points.txt +0 -0
  98. {openrag-0.3.4 → openrag-0.4.0}/src/openrag.egg-info/top_level.txt +0 -0
  99. {openrag-0.3.4 → openrag-0.4.0}/src/services/__init__.py +0 -0
  100. {openrag-0.3.4 → openrag-0.4.0}/src/services/api_key_service.py +0 -0
  101. {openrag-0.3.4 → openrag-0.4.0}/src/services/conversation_persistence_service.py +0 -0
  102. {openrag-0.3.4 → openrag-0.4.0}/src/services/document_service.py +0 -0
  103. {openrag-0.3.4 → openrag-0.4.0}/src/services/knowledge_filter_service.py +0 -0
  104. {openrag-0.3.4 → openrag-0.4.0}/src/services/langflow_file_service.py +0 -0
  105. {openrag-0.3.4 → openrag-0.4.0}/src/services/langflow_history_service.py +0 -0
  106. {openrag-0.3.4 → openrag-0.4.0}/src/services/langflow_mcp_service.py +0 -0
  107. {openrag-0.3.4 → openrag-0.4.0}/src/services/models_service.py +0 -0
  108. {openrag-0.3.4 → openrag-0.4.0}/src/services/monitor_service.py +0 -0
  109. {openrag-0.3.4 → openrag-0.4.0}/src/services/session_ownership_service.py +0 -0
  110. {openrag-0.3.4 → openrag-0.4.0}/src/session_manager.py +0 -0
  111. {openrag-0.3.4 → openrag-0.4.0}/src/tui/__init__.py +0 -0
  112. {openrag-0.3.4 → openrag-0.4.0}/src/tui/_assets/docker-compose.gpu.yml +0 -0
  113. {openrag-0.3.4 → openrag-0.4.0}/src/tui/_assets/flows/components/ollama_embedding.json +0 -0
  114. {openrag-0.3.4 → openrag-0.4.0}/src/tui/_assets/flows/components/ollama_llm.json +0 -0
  115. {openrag-0.3.4 → openrag-0.4.0}/src/tui/_assets/flows/components/ollama_llm_text.json +0 -0
  116. {openrag-0.3.4 → openrag-0.4.0}/src/tui/_assets/flows/components/watsonx_embedding.json +0 -0
  117. {openrag-0.3.4 → openrag-0.4.0}/src/tui/_assets/flows/components/watsonx_llm.json +0 -0
  118. {openrag-0.3.4 → openrag-0.4.0}/src/tui/_assets/flows/components/watsonx_llm_text.json +0 -0
  119. {openrag-0.3.4 → openrag-0.4.0}/src/tui/_assets/openrag-documents/docling.pdf +0 -0
  120. {openrag-0.3.4 → openrag-0.4.0}/src/tui/_assets/openrag-documents/ibm_anthropic.pdf +0 -0
  121. {openrag-0.3.4 → openrag-0.4.0}/src/tui/_assets/openrag-documents/openrag-documentation.pdf +0 -0
  122. {openrag-0.3.4 → openrag-0.4.0}/src/tui/_assets/openrag-documents/warmup_ocr.pdf +0 -0
  123. {openrag-0.3.4 → openrag-0.4.0}/src/tui/cli.py +0 -0
  124. {openrag-0.3.4 → openrag-0.4.0}/src/tui/managers/__init__.py +0 -0
  125. {openrag-0.3.4 → openrag-0.4.0}/src/tui/managers/docling_manager.py +0 -0
  126. {openrag-0.3.4 → openrag-0.4.0}/src/tui/screens/__init__.py +0 -0
  127. {openrag-0.3.4 → openrag-0.4.0}/src/tui/screens/diagnostics.py +0 -0
  128. {openrag-0.3.4 → openrag-0.4.0}/src/tui/screens/logs.py +0 -0
  129. {openrag-0.3.4 → openrag-0.4.0}/src/tui/screens/welcome.py +0 -0
  130. {openrag-0.3.4 → openrag-0.4.0}/src/tui/utils/__init__.py +0 -0
  131. {openrag-0.3.4 → openrag-0.4.0}/src/tui/utils/clipboard.py +0 -0
  132. {openrag-0.3.4 → openrag-0.4.0}/src/tui/utils/platform.py +0 -0
  133. {openrag-0.3.4 → openrag-0.4.0}/src/tui/utils/startup_checks.py +0 -0
  134. {openrag-0.3.4 → openrag-0.4.0}/src/tui/utils/validation.py +0 -0
  135. {openrag-0.3.4 → openrag-0.4.0}/src/tui/widgets/__init__.py +0 -0
  136. {openrag-0.3.4 → openrag-0.4.0}/src/tui/widgets/command_modal.py +0 -0
  137. {openrag-0.3.4 → openrag-0.4.0}/src/tui/widgets/diagnostics_notification.py +0 -0
  138. {openrag-0.3.4 → openrag-0.4.0}/src/tui/widgets/error_notification.py +0 -0
  139. {openrag-0.3.4 → openrag-0.4.0}/src/tui/widgets/factory_reset_warning_modal.py +0 -0
  140. {openrag-0.3.4 → openrag-0.4.0}/src/tui/widgets/flow_backup_warning_modal.py +0 -0
  141. {openrag-0.3.4 → openrag-0.4.0}/src/tui/widgets/prune_options_modal.py +0 -0
  142. {openrag-0.3.4 → openrag-0.4.0}/src/tui/widgets/upgrade_instructions_modal.py +0 -0
  143. {openrag-0.3.4 → openrag-0.4.0}/src/tui/widgets/version_mismatch_warning_modal.py +0 -0
  144. {openrag-0.3.4 → openrag-0.4.0}/src/tui/widgets/waves.py +0 -0
  145. {openrag-0.3.4 → openrag-0.4.0}/src/utils/__init__.py +0 -0
  146. {openrag-0.3.4 → openrag-0.4.0}/src/utils/acl_utils.py +0 -0
  147. {openrag-0.3.4 → openrag-0.4.0}/src/utils/container_utils.py +0 -0
  148. {openrag-0.3.4 → openrag-0.4.0}/src/utils/docling_client.py +0 -0
  149. {openrag-0.3.4 → openrag-0.4.0}/src/utils/document_processing.py +0 -0
  150. {openrag-0.3.4 → openrag-0.4.0}/src/utils/embedding_fields.py +0 -0
  151. {openrag-0.3.4 → openrag-0.4.0}/src/utils/embeddings.py +0 -0
  152. {openrag-0.3.4 → openrag-0.4.0}/src/utils/env_utils.py +0 -0
  153. {openrag-0.3.4 → openrag-0.4.0}/src/utils/gpu_detection.py +0 -0
  154. {openrag-0.3.4 → openrag-0.4.0}/src/utils/hash_utils.py +0 -0
  155. {openrag-0.3.4 → openrag-0.4.0}/src/utils/langflow_headers.py +0 -0
  156. {openrag-0.3.4 → openrag-0.4.0}/src/utils/langflow_utils.py +0 -0
  157. {openrag-0.3.4 → openrag-0.4.0}/src/utils/logging_config.py +0 -0
  158. {openrag-0.3.4 → openrag-0.4.0}/src/utils/opensearch_queries.py +0 -0
  159. {openrag-0.3.4 → openrag-0.4.0}/src/utils/opensearch_utils.py +0 -0
  160. {openrag-0.3.4 → openrag-0.4.0}/src/utils/paths.py +0 -0
  161. {openrag-0.3.4 → openrag-0.4.0}/src/utils/telemetry/__init__.py +0 -0
  162. {openrag-0.3.4 → openrag-0.4.0}/src/utils/telemetry/category.py +0 -0
  163. {openrag-0.3.4 → openrag-0.4.0}/src/utils/telemetry/client.py +0 -0
  164. {openrag-0.3.4 → openrag-0.4.0}/src/utils/telemetry/message_id.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openrag
3
- Version: 0.3.4
3
+ Version: 0.4.0
4
4
  Summary: OpenRAG is a comprehensive Retrieval-Augmented Generation platform that enables intelligent document search and AI-powered conversations.
5
5
  Classifier: Development Status :: 4 - Beta
6
6
  Classifier: Environment :: Console
@@ -28,6 +28,7 @@ Requires-Dist: python-multipart>=0.0.20
28
28
  Requires-Dist: fastapi>=0.115.0
29
29
  Requires-Dist: uvicorn>=0.35.0
30
30
  Requires-Dist: boto3>=1.35.0
31
+ Requires-Dist: ibm-cos-sdk>=2.13.0
31
32
  Requires-Dist: psutil>=7.0.0
32
33
  Requires-Dist: rich>=13.0.0
33
34
  Requires-Dist: textual>=0.45.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "openrag"
7
- version = "0.3.4"
7
+ version = "0.4.0"
8
8
  description = "OpenRAG is a comprehensive Retrieval-Augmented Generation platform that enables intelligent document search and AI-powered conversations."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -34,6 +34,7 @@ dependencies = [
34
34
  "fastapi>=0.115.0",
35
35
  "uvicorn>=0.35.0",
36
36
  "boto3>=1.35.0",
37
+ "ibm-cos-sdk>=2.13.0",
37
38
  "psutil>=7.0.0",
38
39
  "rich>=13.0.0",
39
40
  "textual>=0.45.0",
@@ -43,7 +44,7 @@ dependencies = [
43
44
  "zxcvbn>=4.5.0",
44
45
  "openai>=1.0.0",
45
46
  "pyyaml>=6.0",
46
- "tiktoken>=0.7.0"
47
+ "tiktoken>=0.7.0",
47
48
  ]
48
49
 
49
50
  [dependency-groups]
@@ -815,31 +815,36 @@ async def async_langflow_chat_stream(
815
815
 
816
816
 
817
817
  async def delete_user_conversation(user_id: str, response_id: str) -> bool:
818
- """Delete a conversation for a user from both memory and persistent storage (async, non-blocking)"""
818
+ """Delete a conversation for a user from both memory and persistent storage.
819
+
820
+ Returns:
821
+ True — conversation was found and deleted from at least one store.
822
+ False — conversation did not exist in any store (confirmed not-found).
823
+
824
+ Raises:
825
+ Exception — on unexpected storage errors so callers can distinguish
826
+ a confirmed "not found" from a backend failure.
827
+ """
819
828
  deleted = False
820
829
 
821
- try:
822
- # Delete from in-memory storage
823
- if user_id in active_conversations and response_id in active_conversations[user_id]:
824
- del active_conversations[user_id][response_id]
825
- logger.debug(f"Deleted conversation {response_id} from memory for user {user_id}")
826
- deleted = True
827
-
828
- # Delete from persistent storage
829
- conversation_deleted = await conversation_persistence.delete_conversation_thread(user_id, response_id)
830
- if conversation_deleted:
831
- logger.debug(f"Deleted conversation {response_id} from persistent storage for user {user_id}")
832
- deleted = True
833
-
834
- # Release session ownership
835
- try:
836
- from services.session_ownership_service import session_ownership_service
837
- session_ownership_service.release_session(user_id, response_id)
838
- logger.debug(f"Released session ownership for {response_id} for user {user_id}")
839
- except Exception as e:
840
- logger.warning(f"Failed to release session ownership: {e}")
830
+ # Delete from in-memory storage (cannot raise)
831
+ if user_id in active_conversations and response_id in active_conversations[user_id]:
832
+ del active_conversations[user_id][response_id]
833
+ logger.debug(f"Deleted conversation {response_id} from memory for user {user_id}")
834
+ deleted = True
835
+
836
+ # Delete from persistent storage — let real errors propagate to the caller
837
+ conversation_deleted = await conversation_persistence.delete_conversation_thread(user_id, response_id)
838
+ if conversation_deleted:
839
+ logger.debug(f"Deleted conversation {response_id} from persistent storage for user {user_id}")
840
+ deleted = True
841
841
 
842
- return deleted
842
+ # Release session ownership (best-effort; never masks storage errors above)
843
+ try:
844
+ from services.session_ownership_service import session_ownership_service
845
+ session_ownership_service.release_session(user_id, response_id)
846
+ logger.debug(f"Released session ownership for {response_id} for user {user_id}")
843
847
  except Exception as e:
844
- logger.error(f"Error deleting conversation {response_id} for user {user_id}: {e}")
845
- return False
848
+ logger.warning(f"Failed to release session ownership: {e}")
849
+
850
+ return deleted
@@ -88,6 +88,11 @@ async def get_synced_file_ids_for_connector(
88
88
  class ConnectorSyncBody(BaseModel):
89
89
  max_files: Optional[int] = None
90
90
  selected_files: Optional[List[Any]] = None
91
+ # When True, ingest ALL files from the connector (bypasses the existing-files gate).
92
+ # Used by direct-sync providers like IBM COS on initial ingest.
93
+ sync_all: bool = False
94
+ # When set, only ingest files from these buckets (IBM COS specific).
95
+ bucket_filter: Optional[List[str]] = None
91
96
 
92
97
 
93
98
  async def list_connectors(
@@ -96,8 +101,8 @@ async def list_connectors(
96
101
  ):
97
102
  """List available connector types with metadata"""
98
103
  try:
99
- connector_types = (
100
- connector_service.connection_manager.get_available_connector_types()
104
+ connector_types = connector_service.connection_manager.get_available_connector_types(
105
+ user_id=user.user_id
101
106
  )
102
107
  return JSONResponse({"connectors": connector_types})
103
108
  except Exception as e:
@@ -200,6 +205,51 @@ async def connector_sync(
200
205
  jwt_token=jwt_token,
201
206
  file_infos=file_infos,
202
207
  )
208
+ elif body.sync_all or body.bucket_filter:
209
+ # Full ingest: discover and ingest all files (or files from specific buckets).
210
+ # Used by direct-sync providers (IBM COS) on initial ingest or per-bucket sync.
211
+ logger.info(
212
+ "Full connector ingest requested",
213
+ connector_type=connector_type,
214
+ bucket_filter=body.bucket_filter,
215
+ )
216
+ connector = await connector_service.get_connector(working_connection.connection_id)
217
+ if body.bucket_filter:
218
+ # List only files from the requested buckets, then sync_specific_files
219
+ original_buckets = connector.bucket_names
220
+ connector.bucket_names = body.bucket_filter
221
+ try:
222
+ all_file_ids = []
223
+ page_token = None
224
+ while True:
225
+ result = await connector.list_files(page_token=page_token)
226
+ for f in result.get("files", []):
227
+ all_file_ids.append(f["id"])
228
+ page_token = result.get("next_page_token")
229
+ if not page_token:
230
+ break
231
+ finally:
232
+ connector.bucket_names = original_buckets
233
+
234
+ if not all_file_ids:
235
+ return JSONResponse(
236
+ {"status": "no_files", "message": "No files found in the selected buckets."},
237
+ status_code=200,
238
+ )
239
+ task_id = await connector_service.sync_specific_files(
240
+ working_connection.connection_id,
241
+ user.user_id,
242
+ all_file_ids,
243
+ jwt_token=jwt_token,
244
+ )
245
+ else:
246
+ # sync_all: ingest everything the connector can see
247
+ task_id = await connector_service.sync_connector_files(
248
+ working_connection.connection_id,
249
+ user.user_id,
250
+ max_files=max_files,
251
+ jwt_token=jwt_token,
252
+ )
203
253
  else:
204
254
  # No files specified - sync only files already in OpenSearch for this connector
205
255
  # This ensures deleted files stay deleted
@@ -209,7 +259,7 @@ async def connector_sync(
209
259
  session_manager=session_manager,
210
260
  jwt_token=jwt_token,
211
261
  )
212
-
262
+
213
263
  if not existing_file_ids and not existing_filenames:
214
264
  return JSONResponse(
215
265
  {
@@ -218,7 +268,7 @@ async def connector_sync(
218
268
  },
219
269
  status_code=200,
220
270
  )
221
-
271
+
222
272
  # If we have document_ids (connector file IDs), use sync_specific_files
223
273
  # Otherwise, use filename filtering with sync_connector_files
224
274
  if existing_file_ids:
@@ -602,6 +652,8 @@ async def connector_disconnect(
602
652
  )
603
653
 
604
654
 
655
+ # ---------------------------------------------------------------------------
656
+
605
657
  async def sync_all_connectors(
606
658
  connector_service=Depends(get_connector_service),
607
659
  session_manager=Depends(get_session_manager),
@@ -615,7 +667,7 @@ async def sync_all_connectors(
615
667
  jwt_token = user.jwt_token
616
668
 
617
669
  # Cloud connector types to sync
618
- cloud_connector_types = ["google_drive", "onedrive", "sharepoint"]
670
+ cloud_connector_types = ["google_drive", "onedrive", "sharepoint", "ibm_cos", "aws_s3"]
619
671
 
620
672
  all_task_ids = []
621
673
  synced_connectors = []
@@ -121,16 +121,26 @@ async def check_filename_exists(
121
121
  )
122
122
 
123
123
  from utils.opensearch_queries import build_filename_search_body
124
+ from utils.file_utils import get_filename_aliases
124
125
 
125
- search_body = build_filename_search_body(filename, size=1, source=["filename"])
126
+ candidate_filenames = get_filename_aliases(filename)
127
+ if not candidate_filenames:
128
+ return JSONResponse({"exists": False, "filename": filename}, status_code=200)
126
129
 
127
130
  logger.debug("Checking filename existence", filename=filename, index_name=get_index_name())
131
+ exists = False
128
132
 
129
133
  try:
130
- response = await opensearch_client.search(
131
- index=get_index_name(),
132
- body=search_body
133
- )
134
+ for candidate in candidate_filenames:
135
+ search_body = build_filename_search_body(candidate, size=1, source=["filename"])
136
+ response = await opensearch_client.search(
137
+ index=get_index_name(),
138
+ body=search_body
139
+ )
140
+ hits = response.get("hits", {}).get("hits", [])
141
+ if hits:
142
+ exists = True
143
+ break
134
144
  except Exception as search_err:
135
145
  if "index_not_found_exception" in str(search_err):
136
146
  logger.info("Index does not exist, creating it now before upload")
@@ -138,9 +148,6 @@ async def check_filename_exists(
138
148
  return JSONResponse({"exists": False, "filename": filename}, status_code=200)
139
149
  raise
140
150
 
141
- hits = response.get("hits", {}).get("hits", [])
142
- exists = len(hits) > 0
143
-
144
151
  return JSONResponse({"exists": exists, "filename": filename}, status_code=200)
145
152
 
146
153
  except Exception as e:
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import json
2
3
  import platform
3
4
  from fastapi import Depends, Request, HTTPException
@@ -36,6 +37,7 @@ from dependencies import (
36
37
  from session_manager import User
37
38
 
38
39
  logger = get_logger(__name__)
40
+ _background_tasks: set[asyncio.Task] = set()
39
41
 
40
42
 
41
43
  class SettingsUpdateBody(BaseModel):
@@ -209,6 +211,9 @@ class RollbackResponse(BaseModel):
209
211
  cancelled_tasks: int
210
212
  deleted_files: int
211
213
 
214
+ class RollbackBody(BaseModel):
215
+ embedding_only: bool = False
216
+
212
217
 
213
218
  # Docling preset configurations
214
219
  def get_docling_preset_configs(
@@ -605,6 +610,12 @@ async def update_settings(
605
610
  logger.error(f"Failed to update docling settings in flow: {str(e)}")
606
611
 
607
612
  if body.chunk_size is not None:
613
+ effective_overlap = body.chunk_overlap if body.chunk_overlap is not None else current_config.knowledge.chunk_overlap
614
+ if effective_overlap >= body.chunk_size:
615
+ raise HTTPException(
616
+ status_code=422,
617
+ detail="chunk_overlap must be less than chunk_size"
618
+ )
608
619
  current_config.knowledge.chunk_size = body.chunk_size
609
620
  config_updated = True
610
621
  await TelemetryClient.send_event(
@@ -625,6 +636,12 @@ async def update_settings(
625
636
  # The config will still be saved
626
637
 
627
638
  if body.chunk_overlap is not None:
639
+ effective_chunk_size = body.chunk_size if body.chunk_size is not None else current_config.knowledge.chunk_size
640
+ if body.chunk_overlap >= effective_chunk_size:
641
+ raise HTTPException(
642
+ status_code=422,
643
+ detail="chunk_overlap must be less than chunk_size"
644
+ )
628
645
  current_config.knowledge.chunk_overlap = body.chunk_overlap
629
646
  config_updated = True
630
647
  await TelemetryClient.send_event(
@@ -815,30 +832,31 @@ async def update_settings(
815
832
  {"error": "Failed to save configuration"}, status_code=500
816
833
  )
817
834
 
818
- # Update Langflow global variables and model values if provider settings changed
835
+ # Refresh patched client immediately so subsequent requests pick up latest config.
819
836
  await clients.refresh_patched_client()
820
837
 
838
+ # Run expensive Langflow sync in the background to keep settings updates responsive.
821
839
  if should_validate or provider_updated:
822
- try:
823
- flows_service = _get_flows_service()
824
-
825
- # Update global variables
826
- await _update_langflow_global_variables(current_config, flows_service=flows_service)
827
-
828
- # Update LLM client credentials when embedding selection changes
829
- if body.embedding_provider is not None or body.embedding_model is not None:
830
- await _update_mcp_servers_with_provider_credentials(
831
- current_config, session_manager, flows_service=flows_service
832
- )
833
-
834
- # Update model values if provider or model changed (including removals that trigger fallback)
835
- if body.llm_provider is not None or body.llm_model is not None or body.embedding_provider is not None or body.embedding_model is not None or provider_updated:
836
- await _update_langflow_model_values(current_config, flows_service)
837
-
838
- except Exception as e:
839
- logger.error(f"Failed to update Langflow settings: {str(e)}")
840
- # Don't fail the entire settings update if Langflow update fails
841
- # The config was still saved
840
+ task = asyncio.create_task(
841
+ _run_async_post_save_langflow_updates(
842
+ session_manager=session_manager,
843
+ update_mcp_servers=(
844
+ body.embedding_provider is not None
845
+ or body.embedding_model is not None
846
+ or provider_updated
847
+ ),
848
+ update_model_values=(
849
+ body.llm_provider is not None
850
+ or body.llm_model is not None
851
+ or body.embedding_provider is not None
852
+ or body.embedding_model is not None
853
+ or provider_updated
854
+ ),
855
+ )
856
+ )
857
+ # Keep a strong reference until completion to avoid premature GC cancellation.
858
+ _background_tasks.add(task)
859
+ task.add_done_callback(_background_tasks.discard)
842
860
 
843
861
 
844
862
  set_fields = [k for k, v in body.model_dump().items() if v is not None]
@@ -1380,6 +1398,37 @@ async def _update_langflow_global_variables(config, flows_service=None):
1380
1398
  raise
1381
1399
 
1382
1400
 
1401
+ async def _run_async_post_save_langflow_updates(
1402
+ session_manager,
1403
+ update_mcp_servers: bool,
1404
+ update_model_values: bool,
1405
+ ) -> None:
1406
+ """Apply post-save Langflow synchronization asynchronously."""
1407
+ try:
1408
+ current_config = get_openrag_config()
1409
+ flows_service = _get_flows_service()
1410
+
1411
+ # Update global variables
1412
+ await _update_langflow_global_variables(
1413
+ current_config, flows_service=flows_service
1414
+ )
1415
+
1416
+ # Update LLM client credentials when embedding selection changes
1417
+ if update_mcp_servers:
1418
+ await _update_mcp_servers_with_provider_credentials(
1419
+ current_config, session_manager, flows_service=flows_service
1420
+ )
1421
+
1422
+ # Update model values if provider/model changed (including removals/fallbacks)
1423
+ if update_model_values:
1424
+ await _update_langflow_model_values(current_config, flows_service)
1425
+
1426
+ logger.info("Completed asynchronous Langflow post-save sync")
1427
+ except Exception as e:
1428
+ # Do not fail user request if async sync fails; keep parity with existing behavior.
1429
+ logger.error(f"Failed to update Langflow settings asynchronously: {str(e)}")
1430
+
1431
+
1383
1432
  async def _update_mcp_servers_with_provider_credentials(config, session_manager = None, flows_service=None):
1384
1433
  # Update MCP servers with provider credentials
1385
1434
  try:
@@ -1592,8 +1641,10 @@ async def reapply_all_settings(session_manager = None):
1592
1641
 
1593
1642
  async def rollback_onboarding(
1594
1643
  request: Request,
1644
+ body: Optional[RollbackBody] = None,
1595
1645
  session_manager=Depends(get_session_manager),
1596
1646
  task_service=Depends(get_task_service),
1647
+ knowledge_filter_service=Depends(get_knowledge_filter_service),
1597
1648
  user: User = Depends(get_current_user),
1598
1649
  ) -> RollbackResponse:
1599
1650
  """Rollback onboarding configuration when sample data files fail.
@@ -1623,7 +1674,36 @@ async def rollback_onboarding(
1623
1674
  cancelled_tasks = []
1624
1675
  deleted_files = []
1625
1676
 
1677
+ # Delete knowledge filters created during onboarding
1678
+ try:
1679
+ async def remove_filter(filter_id: Optional[str]):
1680
+ if filter_id and knowledge_filter_service:
1681
+ try:
1682
+ result = await knowledge_filter_service.delete_knowledge_filter(
1683
+ filter_id, user.user_id, user.jwt_token
1684
+ )
1685
+ if result and result.get("success"):
1686
+ logger.info(f"Deleted knowledge filter {filter_id}")
1687
+ else:
1688
+ error_msg = result.get("error") if result else "Unknown error"
1689
+ logger.warning(f"Could not delete knowledge filter {filter_id}: {error_msg}")
1690
+ except Exception as e:
1691
+ logger.warning(f"Exception deleting knowledge filter {filter_id}: {str(e)}")
1692
+
1693
+ if getattr(current_config.onboarding, 'openrag_docs_filter_id', None):
1694
+ await remove_filter(current_config.onboarding.openrag_docs_filter_id)
1695
+ current_config.onboarding.openrag_docs_filter_id = None
1696
+
1697
+ if getattr(current_config.onboarding, 'user_doc_filter_id', None):
1698
+ await remove_filter(current_config.onboarding.user_doc_filter_id)
1699
+ current_config.onboarding.user_doc_filter_id = None
1700
+ except Exception as e:
1701
+ logger.error(f"Error while cleaning up knowledge filters: {e}")
1702
+
1626
1703
  # Cancel all active tasks and collect successfully ingested files
1704
+ from session_manager import AnonymousUser
1705
+ anonymous_user_id = AnonymousUser().user_id
1706
+
1627
1707
  for task_data in all_tasks:
1628
1708
  task_id = task_data.get("task_id")
1629
1709
  task_status = task_data.get("status")
@@ -1638,41 +1718,40 @@ async def rollback_onboarding(
1638
1718
  except Exception as e:
1639
1719
  logger.error(f"Failed to cancel task {task_id}: {str(e)}")
1640
1720
 
1641
- # For completed tasks, find successfully ingested files and delete them
1642
- elif task_status == "completed":
1643
- files = task_data.get("files", {})
1644
- if isinstance(files, dict):
1645
- for file_path, file_info in files.items():
1646
- # Check if file was successfully ingested
1647
- if isinstance(file_info, dict):
1648
- file_status = file_info.get("status")
1649
- filename = file_info.get("filename") or file_path.split("/")[-1]
1650
-
1651
- if file_status == "completed" and filename:
1652
- try:
1653
- # Get user's OpenSearch client
1654
- opensearch_client = session_manager.get_user_opensearch_client(
1655
- user.user_id, jwt_token
1656
- )
1657
-
1658
- # Delete documents by filename
1659
- from utils.opensearch_queries import build_filename_delete_body
1660
- from config.settings import get_index_name
1661
-
1662
- delete_query = build_filename_delete_body(filename)
1663
-
1664
- result = await opensearch_client.delete_by_query(
1665
- index=get_index_name(),
1666
- body=delete_query,
1667
- conflicts="proceed"
1668
- )
1669
-
1670
- deleted_count = result.get("deleted", 0)
1671
- if deleted_count > 0:
1672
- deleted_files.append(filename)
1673
- logger.info(f"Deleted {deleted_count} chunks for filename {filename}")
1674
- except Exception as e:
1675
- logger.error(f"Failed to delete documents for {filename}: {str(e)}")
1721
+ # Delete all files associated with any task, regardless of whether
1722
+ # the task failed or completed, to ensure no partial chunks remain in OpenSearch.
1723
+ files = task_data.get("files", {})
1724
+ if isinstance(files, dict):
1725
+ for file_path, file_info in files.items():
1726
+ if isinstance(file_info, dict):
1727
+ filename = file_info.get("filename") or file_path.split("/")[-1]
1728
+ if filename:
1729
+ try:
1730
+ opensearch_client = session_manager.get_user_opensearch_client(
1731
+ user.user_id, jwt_token
1732
+ )
1733
+ from utils.opensearch_queries import build_filename_delete_body
1734
+ from config.settings import get_index_name
1735
+
1736
+ delete_query = build_filename_delete_body(filename)
1737
+ result = await opensearch_client.delete_by_query(
1738
+ index=get_index_name(),
1739
+ body=delete_query,
1740
+ conflicts="proceed"
1741
+ )
1742
+ deleted_count = result.get("deleted", 0)
1743
+ if deleted_count > 0:
1744
+ deleted_files.append(filename)
1745
+ logger.info(f"Deleted {deleted_count} chunks for filename {filename}")
1746
+ except Exception as e:
1747
+ logger.error(f"Failed to delete documents for {filename}: {str(e)}")
1748
+
1749
+ # Wipe the task completely from memory so the frontend doesn't see it anymore
1750
+ for check_user_id in [user.user_id, anonymous_user_id]:
1751
+ if check_user_id in task_service.task_store and task_id in task_service.task_store[check_user_id]:
1752
+ task_service._task_locks.pop(task_id, None)
1753
+ task_service.task_store[check_user_id].pop(task_id, None)
1754
+ logger.info(f"Purged task {task_id} completely from task_store for user {check_user_id}")
1676
1755
 
1677
1756
  # Clear embedding provider and model settings
1678
1757
  current_config.knowledge.embedding_provider = "openai" # Reset to default
@@ -1680,11 +1759,21 @@ async def rollback_onboarding(
1680
1759
  current_config.onboarding.openrag_docs_ingested_version = None
1681
1760
  current_config.onboarding.openrag_docs_remote_signature = None
1682
1761
 
1762
+ embedding_only = body.embedding_only if body else False
1763
+
1683
1764
  # Mark config as not edited so user can go through onboarding again
1684
- current_config.edited = False
1685
- current_config.onboarding.current_step = 0
1765
+ if not embedding_only:
1766
+ current_config.edited = False
1767
+ current_config.onboarding.current_step = 0
1768
+ # Also clear LLM provider and model settings when doing a full rollback
1769
+ current_config.agent.llm_provider = "openai" # Reset to default
1770
+ current_config.agent.llm_model = ""
1771
+ else:
1772
+ # When rolling back embedding only, we keep edited=True
1773
+ # and set current_step to 1 (which is the embedding step)
1774
+ current_config.onboarding.current_step = 1
1686
1775
 
1687
- # Save the rolled back configuration manually to avoid save_config_file setting edited=True
1776
+ # Save the rolled back configuration manually
1688
1777
  try:
1689
1778
  import yaml
1690
1779
  config_file = config_manager.config_file
@@ -1692,14 +1781,14 @@ async def rollback_onboarding(
1692
1781
  # Ensure directory exists
1693
1782
  config_file.parent.mkdir(parents=True, exist_ok=True)
1694
1783
 
1695
- # Save config with edited=False
1784
+ # Save config with current edited state
1696
1785
  with open(config_file, "w") as f:
1697
1786
  yaml.dump(current_config.to_dict(), f, default_flow_style=False, indent=2)
1698
1787
 
1699
1788
  # Update cached config
1700
1789
  config_manager._config = current_config
1701
1790
 
1702
- logger.info("Successfully saved rolled back configuration with edited=False")
1791
+ logger.info(f"Successfully saved rolled back configuration with edited={current_config.edited}")
1703
1792
  except Exception as e:
1704
1793
  logger.error(f"Failed to save rolled back configuration: {e}")
1705
1794
  return JSONResponse(
@@ -226,6 +226,8 @@ async def chat_delete_endpoint(
226
226
  """Delete a conversation. DELETE /v1/chat/{chat_id}"""
227
227
  try:
228
228
  result = await chat_service.delete_session(user.user_id, chat_id)
229
+ if result.get("not_found"):
230
+ return JSONResponse({"error": "Conversation not found"}, status_code=404)
229
231
  if result.get("success"):
230
232
  return JSONResponse({"success": True})
231
233
  else:
@@ -50,6 +50,8 @@ GOOGLE_OAUTH_CLIENT_ID = os.getenv("GOOGLE_OAUTH_CLIENT_ID")
50
50
  GOOGLE_OAUTH_CLIENT_SECRET = os.getenv("GOOGLE_OAUTH_CLIENT_SECRET")
51
51
  DOCLING_OCR_ENGINE = os.getenv("DOCLING_OCR_ENGINE")
52
52
 
53
+ IBM_AUTH_ENABLED = os.getenv("IBM_AUTH_ENABLED", "false").lower() in ("true", "1", "yes")
54
+
53
55
  # Ingestion configuration
54
56
  DISABLE_INGEST_WITH_LANGFLOW = os.getenv(
55
57
  "DISABLE_INGEST_WITH_LANGFLOW", "false"
@@ -2,10 +2,14 @@ from .base import BaseConnector
2
2
  from .google_drive import GoogleDriveConnector
3
3
  from .sharepoint import SharePointConnector
4
4
  from .onedrive import OneDriveConnector
5
+ from .ibm_cos import IBMCOSConnector
6
+ from .aws_s3 import S3Connector
5
7
 
6
8
  __all__ = [
7
9
  "BaseConnector",
8
10
  "GoogleDriveConnector",
9
11
  "SharePointConnector",
10
12
  "OneDriveConnector",
13
+ "IBMCOSConnector",
14
+ "S3Connector",
11
15
  ]
@@ -0,0 +1,19 @@
1
+ """Amazon S3 / S3-compatible connector for OpenRAG."""
2
+
3
+ from .connector import S3Connector
4
+ from .models import S3ConfigureBody
5
+ from .api import (
6
+ s3_defaults,
7
+ s3_configure,
8
+ s3_list_buckets,
9
+ s3_bucket_status,
10
+ )
11
+
12
+ __all__ = [
13
+ "S3Connector",
14
+ "S3ConfigureBody",
15
+ "s3_defaults",
16
+ "s3_configure",
17
+ "s3_list_buckets",
18
+ "s3_bucket_status",
19
+ ]