OpenRAG 0.8.0.dev20__tar.gz → 0.8.1__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 (278) hide show
  1. {openrag-0.8.0.dev20/src/openrag.egg-info → openrag-0.8.1}/PKG-INFO +10 -7
  2. {openrag-0.8.0.dev20 → openrag-0.8.1}/pyproject.toml +15 -7
  3. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/agent.py +141 -91
  4. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/connectors.py +74 -2
  5. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/docling.py +26 -3
  6. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/langflow_files.py +17 -4
  7. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/models.py +151 -23
  8. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/provider_health.py +237 -3
  9. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/provider_validation.py +273 -44
  10. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/router.py +97 -6
  11. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/settings/endpoints.py +140 -18
  12. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/settings/langflow_sync.py +5 -1
  13. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/settings/models.py +60 -0
  14. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/upload.py +30 -5
  15. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/v1/documents.py +4 -0
  16. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/v1/llm.py +44 -8
  17. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/v1/search.py +13 -8
  18. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/app/lifespan.py +7 -0
  19. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/app/routes/internal.py +6 -0
  20. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/config/config_manager.py +228 -49
  21. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/config/model_providers.py +13 -0
  22. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/config/model_providers.yaml +41 -21
  23. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/config/settings.py +205 -11
  24. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/service.py +10 -2
  25. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/migrations_runtime.py +1 -0
  26. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/models/session_ownership.py +2 -0
  27. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/repositories/permission_repo.py +3 -6
  28. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/repositories/session_ownership_repo.py +32 -12
  29. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/dependencies.py +9 -2
  30. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/models/processors.py +340 -211
  31. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/models/url.py +3 -0
  32. openrag-0.8.1/src/observability/litellm_langfuse.py +89 -0
  33. openrag-0.8.1/src/observability/spans.py +185 -0
  34. openrag-0.8.1/src/observability/tracing.py +221 -0
  35. {openrag-0.8.0.dev20 → openrag-0.8.1/src/openrag.egg-info}/PKG-INFO +10 -7
  36. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/openrag.egg-info/SOURCES.txt +4 -0
  37. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/openrag.egg-info/requires.txt +10 -6
  38. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/chat_service.py +19 -4
  39. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/conversation_persistence_service.py +24 -33
  40. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/default_docs_service.py +6 -0
  41. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/docling_service.py +328 -23
  42. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/document_service.py +4 -8
  43. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/flows_service.py +68 -23
  44. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/langflow_file_service.py +37 -12
  45. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/langflow_llm_token_service.py +18 -0
  46. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/langflow_mcp_service.py +1 -0
  47. openrag-0.8.1/src/services/llm_gateway.py +2131 -0
  48. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/model_catalog.py +118 -20
  49. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/provider_error_log.py +5 -1
  50. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/search_service.py +148 -74
  51. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/session_ownership_service.py +182 -23
  52. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/status_checks.py +44 -17
  53. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/status_service.py +5 -1
  54. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/task_service.py +126 -7
  55. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/_assets/docker-compose.yml +17 -5
  56. openrag-0.8.1/src/tui/_assets/flows/ingestion_flow.json +5154 -0
  57. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/_assets/flows/openrag_agent.json +2 -2
  58. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/_assets/flows/openrag_nudges.json +2 -2
  59. openrag-0.8.1/src/tui/_assets/flows/openrag_url_mcp.json +6830 -0
  60. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/config_fields.py +18 -3
  61. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/managers/env_manager.py +16 -3
  62. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/document_processing.py +63 -2
  63. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/langflow_headers.py +5 -2
  64. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/langflow_utils.py +1 -0
  65. openrag-0.8.1/src/utils/upload_limits.py +167 -0
  66. openrag-0.8.0.dev20/src/services/llm_gateway.py +0 -1128
  67. openrag-0.8.0.dev20/src/tui/_assets/flows/ingestion_flow.json +0 -5145
  68. openrag-0.8.0.dev20/src/tui/_assets/flows/openrag_url_mcp.json +0 -6823
  69. {openrag-0.8.0.dev20 → openrag-0.8.1}/LICENSE +0 -0
  70. {openrag-0.8.0.dev20 → openrag-0.8.1}/MANIFEST.in +0 -0
  71. {openrag-0.8.0.dev20 → openrag-0.8.1}/README.md +0 -0
  72. {openrag-0.8.0.dev20 → openrag-0.8.1}/setup.cfg +0 -0
  73. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/__init__.py +0 -0
  74. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/acl.py +0 -0
  75. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/auth.py +0 -0
  76. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/chat.py +0 -0
  77. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/config.py +0 -0
  78. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/documents.py +0 -0
  79. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/files.py +0 -0
  80. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/flows.py +0 -0
  81. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/health.py +0 -0
  82. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/ingest_preview.py +0 -0
  83. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/keys.py +0 -0
  84. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/knowledge_filter.py +0 -0
  85. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/langflow_ingest.py +0 -0
  86. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/nudges.py +0 -0
  87. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/oidc.py +0 -0
  88. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/schemas/status.py +0 -0
  89. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/schemas/tasks.py +0 -0
  90. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/search.py +0 -0
  91. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/settings/__init__.py +0 -0
  92. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/settings/helpers.py +0 -0
  93. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/status_utils.py +0 -0
  94. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/tasks.py +0 -0
  95. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/users.py +0 -0
  96. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/v1/__init__.py +0 -0
  97. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/v1/_filter_resolution.py +0 -0
  98. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/v1/chat.py +0 -0
  99. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/v1/files.py +0 -0
  100. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/v1/knowledge_filters.py +0 -0
  101. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/v1/models.py +0 -0
  102. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/v1/settings.py +0 -0
  103. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/v1/status.py +0 -0
  104. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/v2/__init__.py +0 -0
  105. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/api/v2/files.py +0 -0
  106. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/app/__init__.py +0 -0
  107. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/app/container.py +0 -0
  108. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/app/factory.py +0 -0
  109. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/app/middleware.py +0 -0
  110. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/app/router_app.py +0 -0
  111. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/app/routes/__init__.py +0 -0
  112. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/app/routes/mcp.py +0 -0
  113. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/app/routes/public_v1.py +0 -0
  114. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/app/routes/public_v2.py +0 -0
  115. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/auth/__init__.py +0 -0
  116. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/auth/ibm_auth.py +0 -0
  117. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/auth/jwt_roles.py +0 -0
  118. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/auth/permission_dependencies.py +0 -0
  119. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/auth/request_identity.py +0 -0
  120. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/auth/user_identity_cache.py +0 -0
  121. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/auth_context.py +0 -0
  122. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/bootstrap.py +0 -0
  123. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/config/__init__.py +0 -0
  124. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/config/embedding_constants.py +0 -0
  125. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/config/legacy_prompts.py +0 -0
  126. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/config/model_constants.py +0 -0
  127. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/config/paths.py +0 -0
  128. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/config/storage_mode.py +0 -0
  129. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/config/utils.py +0 -0
  130. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/__init__.py +0 -0
  131. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/aws_s3/__init__.py +0 -0
  132. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/aws_s3/api.py +0 -0
  133. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/aws_s3/auth.py +0 -0
  134. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/aws_s3/connector.py +0 -0
  135. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/aws_s3/models.py +0 -0
  136. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/aws_s3/support.py +0 -0
  137. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/base.py +0 -0
  138. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/chunk_cleanup.py +0 -0
  139. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/connection_manager.py +0 -0
  140. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/google_drive/__init__.py +0 -0
  141. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/google_drive/connector.py +0 -0
  142. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/google_drive/oauth.py +0 -0
  143. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/google_drive_acl.py +0 -0
  144. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/microsoft_graph_acl.py +0 -0
  145. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/microsoft_oauth_utils.py +0 -0
  146. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/onedrive/__init__.py +0 -0
  147. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/onedrive/connector.py +0 -0
  148. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/onedrive/oauth.py +0 -0
  149. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/registry.py +0 -0
  150. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/sharepoint/__init__.py +0 -0
  151. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/sharepoint/connector.py +0 -0
  152. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/sharepoint/oauth.py +0 -0
  153. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/connectors/sharepoint/utils.py +0 -0
  154. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/__init__.py +0 -0
  155. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/base.py +0 -0
  156. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/engine.py +0 -0
  157. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/models/__init__.py +0 -0
  158. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/models/api_key.py +0 -0
  159. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/models/audit_log.py +0 -0
  160. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/models/conversation.py +0 -0
  161. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/models/migration_status.py +0 -0
  162. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/models/permission.py +0 -0
  163. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/models/role.py +0 -0
  164. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/models/role_permission.py +0 -0
  165. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/models/user.py +0 -0
  166. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/models/user_preferences.py +0 -0
  167. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/models/user_role.py +0 -0
  168. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/models/workspace_config.py +0 -0
  169. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/repositories/__init__.py +0 -0
  170. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/repositories/_helpers.py +0 -0
  171. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/repositories/api_key_repo.py +0 -0
  172. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/repositories/audit_repo.py +0 -0
  173. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/repositories/conversation_repo.py +0 -0
  174. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/repositories/preferences_repo.py +0 -0
  175. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/repositories/role_repo.py +0 -0
  176. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/repositories/user_repo.py +0 -0
  177. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/repositories/workspace_config_repo.py +0 -0
  178. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/seed.py +0 -0
  179. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/db/types.py +0 -0
  180. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/main.py +0 -0
  181. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/mcp_http/__init__.py +0 -0
  182. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/mcp_http/server.py +0 -0
  183. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/models/__init__.py +0 -0
  184. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/models/tasks.py +0 -0
  185. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/observability/__init__.py +0 -0
  186. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/observability/instana_boot.py +0 -0
  187. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/openrag.egg-info/dependency_links.txt +0 -0
  188. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/openrag.egg-info/entry_points.txt +0 -0
  189. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/openrag.egg-info/top_level.txt +0 -0
  190. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/__init__.py +0 -0
  191. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/api_key_service.py +0 -0
  192. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/auth_service.py +0 -0
  193. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/component_logs.py +0 -0
  194. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/connector_access_service.py +0 -0
  195. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/connector_oauth_config_service.py +0 -0
  196. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/default_role_sync.py +0 -0
  197. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/dev_role_toggle.py +0 -0
  198. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/dls_principal_service.py +0 -0
  199. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/docling_polling_service.py +0 -0
  200. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/document_index_writer.py +0 -0
  201. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/file_service.py +0 -0
  202. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/file_service_v2.py +0 -0
  203. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/group_acl_service.py +0 -0
  204. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/ingest_preview_service.py +0 -0
  205. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/knowledge_filter_service.py +0 -0
  206. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/langflow_history_service.py +0 -0
  207. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/langflow_ingest_token_service.py +0 -0
  208. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/models_service.py +0 -0
  209. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/monitor_service.py +0 -0
  210. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/onboarding_reset.py +0 -0
  211. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/rbac_service.py +0 -0
  212. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/startup_orchestrator.py +0 -0
  213. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/status_diagnostics.py +0 -0
  214. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/user_service.py +0 -0
  215. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/services/workspace_config_service.py +0 -0
  216. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/session_manager.py +0 -0
  217. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/__init__.py +0 -0
  218. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/_assets/docker-compose.gpu.yml +0 -0
  219. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/_assets/openrag-documents/docling.pdf +0 -0
  220. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/_assets/openrag-documents/ibm_anthropic.pdf +0 -0
  221. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/_assets/openrag-documents/warmup_ocr.pdf +0 -0
  222. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/cli.py +0 -0
  223. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/main.py +0 -0
  224. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/managers/__init__.py +0 -0
  225. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/managers/container_manager.py +0 -0
  226. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/managers/docling_manager.py +0 -0
  227. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/screens/__init__.py +0 -0
  228. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/screens/config.py +0 -0
  229. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/screens/diagnostics.py +0 -0
  230. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/screens/logs.py +0 -0
  231. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/screens/monitor.py +0 -0
  232. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/screens/welcome.py +0 -0
  233. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/utils/__init__.py +0 -0
  234. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/utils/clipboard.py +0 -0
  235. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/utils/platform.py +0 -0
  236. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/utils/startup_checks.py +0 -0
  237. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/utils/validation.py +0 -0
  238. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/utils/version_check.py +0 -0
  239. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/widgets/__init__.py +0 -0
  240. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/widgets/command_modal.py +0 -0
  241. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/widgets/diagnostics_notification.py +0 -0
  242. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/widgets/error_notification.py +0 -0
  243. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/widgets/factory_reset_warning_modal.py +0 -0
  244. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/widgets/flow_backup_warning_modal.py +0 -0
  245. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/widgets/prune_options_modal.py +0 -0
  246. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/widgets/upgrade_instructions_modal.py +0 -0
  247. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/widgets/version_mismatch_warning_modal.py +0 -0
  248. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/tui/widgets/waves.py +0 -0
  249. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/__init__.py +0 -0
  250. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/acl_utils.py +0 -0
  251. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/container_utils.py +0 -0
  252. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/embedding_fields.py +0 -0
  253. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/embeddings.py +0 -0
  254. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/encryption.py +0 -0
  255. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/env_utils.py +0 -0
  256. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/file_utils.py +0 -0
  257. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/filename_claims.py +0 -0
  258. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/gpu_detection.py +0 -0
  259. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/group_acl.py +0 -0
  260. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/hash_utils.py +0 -0
  261. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/ingest_preview_flag.py +0 -0
  262. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/jwt_keygen.py +0 -0
  263. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/jwt_verification.py +0 -0
  264. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/logging_config.py +0 -0
  265. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/opensearch_delete.py +0 -0
  266. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/opensearch_filenames.py +0 -0
  267. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/opensearch_init.py +0 -0
  268. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/opensearch_queries.py +0 -0
  269. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/opensearch_utils.py +0 -0
  270. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/paths.py +0 -0
  271. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/provider_health_cache.py +0 -0
  272. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/run_mode_utils.py +0 -0
  273. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/telemetry/__init__.py +0 -0
  274. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/telemetry/category.py +0 -0
  275. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/telemetry/client.py +0 -0
  276. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/telemetry/message_id.py +0 -0
  277. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/url_content_fetcher.py +0 -0
  278. {openrag-0.8.0.dev20 → openrag-0.8.1}/src/utils/version_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openrag
3
- Version: 0.8.0.dev20
3
+ Version: 0.8.1
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
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: agentd>=0.8.7
19
19
  Requires-Dist: aiofiles>=24.1.0
20
- Requires-Dist: authlib<2.0.0,>=1.7.2
20
+ Requires-Dist: authlib<2.0.0,>=1.8.0
21
21
  Requires-Dist: aiohttp>=3.14.1
22
22
  Requires-Dist: cryptography>=48.0.1
23
23
  Requires-Dist: click>=8.3.3
@@ -29,7 +29,7 @@ Requires-Dist: msal>=1.29.0
29
29
  Requires-Dist: httpx>=0.27.0
30
30
  Requires-Dist: idna>=3.18
31
31
  Requires-Dist: opensearch-py[async]>=3.0.0
32
- Requires-Dist: pyjwt>=2.13.0
32
+ Requires-Dist: pyjwt>=2.15.1
33
33
  Requires-Dist: python-multipart>=0.0.31
34
34
  Requires-Dist: fastapi>=0.115.0
35
35
  Requires-Dist: starlette>=1.6.0
@@ -45,19 +45,20 @@ Requires-Dist: textual-fspicker>=0.6.0
45
45
  Requires-Dist: structlog>=25.4.0
46
46
  Requires-Dist: zxcvbn>=4.5.0
47
47
  Requires-Dist: prometheus-fastapi-instrumentator>=8.0.0
48
- Requires-Dist: litellm<2.0.0,>=1.96.2
48
+ Requires-Dist: litellm<2.0.0,>=1.105.0
49
49
  Requires-Dist: joserfc<2.0.0,>=1.7.3
50
50
  Requires-Dist: pyyaml>=6.0
51
51
  Requires-Dist: tiktoken>=0.7.0
52
52
  Requires-Dist: fastmcp>=3.4.0
53
- Requires-Dist: mcp>=1.28.1
54
- Requires-Dist: openai==2.24.0
53
+ Requires-Dist: mcp>=1.30.0
54
+ Requires-Dist: openai<3,>=2.24.0
55
55
  Requires-Dist: tenacity>=9.1.4
56
56
  Requires-Dist: pygments>=2.20.0
57
57
  Requires-Dist: pyasn1>=0.6.4
58
58
  Requires-Dist: protobuf<8,>=7.35.0
59
59
  Requires-Dist: requests>=2.33.0
60
- Requires-Dist: urllib3>=2.7.0
60
+ Requires-Dist: urllib3>=2.8.0
61
+ Requires-Dist: fsspec>=2026.6.0
61
62
  Requires-Dist: sqlmodel>=0.0.22
62
63
  Requires-Dist: sqlalchemy[asyncio]>=2.0.36
63
64
  Requires-Dist: aiosqlite>=0.20.0
@@ -66,6 +67,8 @@ Requires-Dist: asyncpg>=0.30.0
66
67
  Requires-Dist: cachetools>=5.5.0
67
68
  Provides-Extra: apm
68
69
  Requires-Dist: instana<4,>=3.17.0; extra == "apm"
70
+ Provides-Extra: tracing
71
+ Requires-Dist: langfuse<5,>=4.16; extra == "tracing"
69
72
  Dynamic: license-file
70
73
 
71
74
  <div align="center">
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "openrag"
7
- version = "0.8.0.dev20"
7
+ version = "0.8.1"
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"
@@ -23,7 +23,7 @@ classifiers = [
23
23
  dependencies = [
24
24
  "agentd>=0.8.7",
25
25
  "aiofiles>=24.1.0",
26
- "authlib>=1.7.2,<2.0.0",
26
+ "authlib>=1.8.0,<2.0.0",
27
27
  "aiohttp>=3.14.1",
28
28
  "cryptography>=48.0.1",
29
29
  "click>=8.3.3",
@@ -35,7 +35,7 @@ dependencies = [
35
35
  "httpx>=0.27.0",
36
36
  "idna>=3.18",
37
37
  "opensearch-py[async]>=3.0.0",
38
- "pyjwt>=2.13.0",
38
+ "pyjwt>=2.15.1",
39
39
  "python-multipart>=0.0.31",
40
40
  "fastapi>=0.115.0",
41
41
  "starlette>=1.6.0",
@@ -51,19 +51,20 @@ dependencies = [
51
51
  "structlog>=25.4.0",
52
52
  "zxcvbn>=4.5.0",
53
53
  "prometheus-fastapi-instrumentator>=8.0.0",
54
- "litellm>=1.96.2,<2.0.0",
54
+ "litellm>=1.105.0,<2.0.0",
55
55
  "joserfc>=1.7.3,<2.0.0",
56
56
  "pyyaml>=6.0",
57
57
  "tiktoken>=0.7.0",
58
58
  "fastmcp>=3.4.0",
59
- "mcp>=1.28.1",
60
- "openai==2.24.0",
59
+ "mcp>=1.30.0",
60
+ "openai>=2.24.0,<3",
61
61
  "tenacity>=9.1.4",
62
62
  "pygments>=2.20.0",
63
63
  "pyasn1>=0.6.4",
64
64
  "protobuf>=7.35.0,<8",
65
65
  "requests>=2.33.0",
66
- "urllib3>=2.7.0",
66
+ "urllib3>=2.8.0",
67
+ "fsspec>=2026.6.0",
67
68
  "sqlmodel>=0.0.22",
68
69
  "sqlalchemy[asyncio]>=2.0.36",
69
70
  "aiosqlite>=0.20.0",
@@ -83,6 +84,9 @@ dependencies = [
83
84
  # be adopted explicitly, not resolved into an install.
84
85
  # src/main.py imports it defensively, so its absence downgrades to a warning.
85
86
  apm = ["instana>=3.17.0,<4"]
87
+ tracing = [
88
+ "langfuse>=4.16,<5",
89
+ ]
86
90
 
87
91
  [dependency-groups]
88
92
  dev = [
@@ -92,6 +96,10 @@ dev = [
92
96
  "pytest-cov>=4.0.0",
93
97
  "ruff>=0.15.12",
94
98
  "mypy>=2.0.0",
99
+ # The tracing extra, so its unit tests (tests/unit/observability) run.
100
+ "langfuse>=4.16,<5",
101
+ # Used by the Langflow Split Text component, so its unit tests run the real splitter.
102
+ "langchain-text-splitters>=1.1.2,<2",
95
103
  ]
96
104
 
97
105
  [project.scripts]
@@ -1,6 +1,10 @@
1
+ import asyncio
1
2
  import uuid
3
+ from contextlib import aclosing
2
4
  from typing import Any
3
5
 
6
+ import anyio
7
+
4
8
  from config.config_manager import DEFAULT_SYSTEM_PROMPT
5
9
  from services.conversation_persistence_service import conversation_persistence
6
10
  from utils.langflow_utils import parse_knowledge_chunks, strip_untrusted_fence_recursive
@@ -91,13 +95,10 @@ async def _persist_error_conversation(
91
95
 
92
96
  from services.session_ownership_service import session_ownership_service
93
97
 
98
+ if not await session_ownership_service.claim_session(user_id, store_id):
99
+ raise RuntimeError("Unable to establish chat session ownership")
94
100
  conversation_state["last_activity"] = datetime.now()
95
101
  await store_conversation_thread(user_id, store_id, conversation_state)
96
- try:
97
- await session_ownership_service.claim_session(user_id, store_id)
98
- logger.debug(f"Claimed session {store_id} for user {user_id} after stream error")
99
- except Exception as e:
100
- logger.warning(f"Failed to claim session ownership after stream error: {e}")
101
102
 
102
103
 
103
104
  def _stream_error_chunk(message: str, response_id: str | None = None) -> bytes:
@@ -228,6 +229,7 @@ async def async_response_stream(
228
229
  ):
229
230
  logger.info("User prompt received", prompt=prompt)
230
231
 
232
+ response = None
231
233
  try:
232
234
  # Build request parameters
233
235
  request_params = {
@@ -422,6 +424,10 @@ async def async_response_stream(
422
424
  except Exception:
423
425
  logger.exception("[AGENT] Streaming failed")
424
426
  raise
427
+ finally:
428
+ if response is not None:
429
+ with anyio.CancelScope(shield=True):
430
+ await response.close()
425
431
 
426
432
 
427
433
  # Generic async response function for non-streaming
@@ -494,15 +500,18 @@ async def async_stream(
494
500
  previous_response_id: str = None,
495
501
  log_prefix: str = "response",
496
502
  ):
497
- async for chunk in async_response_stream(
498
- client,
499
- prompt,
500
- model,
501
- extra_headers=extra_headers,
502
- previous_response_id=previous_response_id,
503
- log_prefix=log_prefix,
504
- ):
505
- yield chunk
503
+ async with aclosing(
504
+ async_response_stream(
505
+ client,
506
+ prompt,
507
+ model,
508
+ extra_headers=extra_headers,
509
+ previous_response_id=previous_response_id,
510
+ log_prefix=log_prefix,
511
+ )
512
+ ) as stream:
513
+ async for chunk in stream:
514
+ yield chunk
506
515
 
507
516
 
508
517
  # Async langflow function (non-streaming only)
@@ -718,6 +727,7 @@ async def async_chat_stream(
718
727
  await _persist_error_conversation(user_id, store_id, conversation_state)
719
728
  except Exception as store_error:
720
729
  logger.error(f"Failed to store error conversation: {store_error}")
730
+ store_id = None
721
731
  yield _stream_error_chunk(display_text, store_id)
722
732
 
723
733
 
@@ -850,18 +860,13 @@ async def async_langflow_chat(
850
860
 
851
861
  # Store the conversation thread with its response_id
852
862
  if response_id:
863
+ from services.session_ownership_service import session_ownership_service
864
+
865
+ if not await session_ownership_service.claim_session(user_id, response_id):
866
+ raise RuntimeError("Unable to establish chat session ownership")
853
867
  conversation_state["last_activity"] = datetime.now()
854
868
  await store_conversation_thread(user_id, response_id, conversation_state)
855
869
 
856
- # Claim session ownership for this user
857
- try:
858
- from services.session_ownership_service import session_ownership_service
859
-
860
- await session_ownership_service.claim_session(user_id, response_id)
861
- logger.debug(f"Claimed session {response_id} for user {user_id}")
862
- except Exception as e:
863
- logger.warning(f"Failed to claim session ownership: {e}")
864
-
865
870
  logger.debug(
866
871
  "Stored langflow conversation thread",
867
872
  user_id=user_id,
@@ -894,6 +899,16 @@ async def async_langflow_chat_stream(
894
899
  # conversation_id keeps the OpenRAG sidebar thread; previous_response_id chains
895
900
  # the Langflow session. After errors the client omits previous_response_id.
896
901
  thread_id = conversation_id or previous_response_id
902
+ if not _conversation_thread_in_memory(user_id, thread_id):
903
+ # Clients may continue using the latest Langflow id without a sidebar id.
904
+ thread_id = next(
905
+ (
906
+ stored_id
907
+ for stored_id, state in active_conversations.get(user_id, {}).items()
908
+ if thread_id and thread_id in state.get("response_ids", [])
909
+ ),
910
+ thread_id,
911
+ )
897
912
  previous_loaded_from_memory = _conversation_thread_in_memory(user_id, thread_id)
898
913
  conversation_state = get_conversation_thread(user_id, thread_id)
899
914
 
@@ -915,30 +930,74 @@ async def async_langflow_chat_stream(
915
930
  usage_data = None
916
931
  collected_chunks = [] # Store all chunks for function call data
917
932
  error_occurred = False
933
+ store_id = None
934
+ claimed_ids: set[str] = set()
935
+ turn_finished = False
936
+
937
+ async def persist_stream_state(*session_ids, mint_if_missing=False):
938
+ nonlocal store_id
939
+ from services.session_ownership_service import session_ownership_service
940
+
941
+ if store_id is None:
942
+ store_id = _resolve_conversation_store_id(
943
+ response_id,
944
+ thread_id,
945
+ previous_loaded_from_memory=previous_loaded_from_memory,
946
+ mint_if_missing=mint_if_missing,
947
+ )
948
+ if store_id is None:
949
+ return
950
+ for session_id in dict.fromkeys((store_id, *session_ids)):
951
+ if not session_id or session_id in claimed_ids:
952
+ continue
953
+ if not await session_ownership_service.claim_session(
954
+ user_id, session_id, conversation_id=store_id
955
+ ):
956
+ raise RuntimeError("Unable to establish chat session ownership")
957
+ claimed_ids.add(session_id)
958
+ response_ids = conversation_state.setdefault("response_ids", [])
959
+ if session_id not in response_ids:
960
+ response_ids.append(session_id)
961
+ conversation_state["last_activity"] = datetime.now()
962
+ await store_conversation_thread(user_id, store_id, conversation_state)
963
+
964
+ upstream = async_stream(
965
+ langflow_client,
966
+ prompt,
967
+ flow_id,
968
+ extra_headers=extra_headers,
969
+ previous_response_id=langflow_previous_response_id,
970
+ log_prefix="langflow",
971
+ )
918
972
 
919
973
  try:
920
- async for chunk in async_stream(
921
- langflow_client,
922
- prompt,
923
- flow_id,
924
- extra_headers=extra_headers,
925
- previous_response_id=langflow_previous_response_id,
926
- log_prefix="langflow",
927
- ):
974
+ async for chunk in upstream:
928
975
  # Extract text content to build full response for history
976
+ chunk_ids = []
929
977
  try:
930
978
  import json
931
979
 
932
980
  chunk_data = json.loads(chunk.decode("utf-8"))
933
981
  collected_chunks.append(chunk_data) # Collect all chunk data
934
982
 
983
+ # Own every response/session ID callers can use for continuation,
984
+ # not tool item IDs. OpenAI events may nest the ID in response.
985
+ response_obj = chunk_data.get("response")
986
+ if not isinstance(response_obj, dict):
987
+ response_obj = {}
988
+ chunk_ids = [
989
+ value
990
+ for value in (
991
+ chunk_data.get("id"),
992
+ chunk_data.get("response_id"),
993
+ response_obj.get("id"),
994
+ )
995
+ if isinstance(value, str) and value
996
+ ]
997
+ if chunk_ids:
998
+ response_id = chunk_ids[0]
935
999
  if "delta" in chunk_data and "content" in chunk_data["delta"]:
936
1000
  full_response += chunk_data["delta"]["content"]
937
- # Extract response_id from chunk
938
- if "id" in chunk_data:
939
- response_id = chunk_data["id"]
940
- elif "response_id" in chunk_data:
941
- response_id = chunk_data["response_id"]
942
1001
 
943
1002
  # Check for error status. Do not forward bare Langflow error
944
1003
  # chunks — they often lack a useful message and the client would
@@ -972,6 +1031,10 @@ async def async_langflow_chat_stream(
972
1031
  usage_data = response_obj.get("usage")
973
1032
  except Exception:
974
1033
  pass
1034
+ # This await must stay outside the permissive JSON parser above.
1035
+ # Persist/claim BEFORE yielding the first occurrence of any ID.
1036
+ if any(session_id not in claimed_ids for session_id in chunk_ids):
1037
+ await persist_stream_state(*chunk_ids)
975
1038
  yield chunk
976
1039
 
977
1040
  from api.provider_validation import (
@@ -994,16 +1057,8 @@ async def async_langflow_chat_stream(
994
1057
  "error": True,
995
1058
  }
996
1059
  )
997
- store_id = _resolve_conversation_store_id(
998
- response_id,
999
- thread_id,
1000
- previous_loaded_from_memory=previous_loaded_from_memory,
1001
- mint_if_missing=True,
1002
- )
1003
- try:
1004
- await _persist_error_conversation(user_id, store_id, conversation_state)
1005
- except Exception as store_error:
1006
- logger.error(f"Failed to store error conversation: {store_error}")
1060
+ turn_finished = True
1061
+ await persist_stream_state(response_id, mint_if_missing=True)
1007
1062
  yield _stream_error_chunk(display_text, store_id)
1008
1063
  return
1009
1064
 
@@ -1020,20 +1075,29 @@ async def async_langflow_chat_stream(
1020
1075
  assistant_message["response_data"] = {"usage": usage_data}
1021
1076
  conversation_state["messages"].append(assistant_message)
1022
1077
 
1023
- persist_id = _resolve_conversation_store_id(
1024
- response_id,
1025
- thread_id,
1026
- previous_loaded_from_memory=previous_loaded_from_memory,
1027
- )
1028
- if persist_id:
1029
- conversation_state["last_activity"] = datetime.now()
1030
- await store_conversation_thread(user_id, persist_id, conversation_state)
1078
+ turn_finished = True
1079
+ await persist_stream_state(response_id)
1080
+ except (asyncio.CancelledError, GeneratorExit):
1081
+ # Disconnects do not run the normal tail and are not provider failures.
1082
+ # Keep exactly one partial assistant turn and propagate cancellation;
1083
+ # never replay the prompt or synthesize a terminal event to a dead client.
1084
+ with anyio.CancelScope(shield=True):
1085
+ if not turn_finished and full_response:
1086
+ conversation_state["messages"].append(
1087
+ {
1088
+ "role": "assistant",
1089
+ "content": full_response,
1090
+ "response_id": response_id,
1091
+ "timestamp": datetime.now(),
1092
+ "chunks": collected_chunks,
1093
+ "interrupted": True,
1094
+ }
1095
+ )
1031
1096
  try:
1032
- from services.session_ownership_service import session_ownership_service
1033
-
1034
- await session_ownership_service.claim_session(user_id, persist_id)
1035
- except Exception as e:
1036
- logger.warning(f"Failed to claim session ownership: {e}")
1097
+ await persist_stream_state(response_id)
1098
+ except Exception:
1099
+ logger.exception("Failed to store interrupted Langflow turn")
1100
+ raise
1037
1101
  except Exception as e:
1038
1102
  logger.error(f"Error in langflow chat stream: {e}", exc_info=True)
1039
1103
  from api.provider_validation import resolve_chat_stream_error_message_async
@@ -1051,17 +1115,16 @@ async def async_langflow_chat_stream(
1051
1115
  "error": True,
1052
1116
  }
1053
1117
  )
1054
- store_id = _resolve_conversation_store_id(
1055
- response_id,
1056
- thread_id,
1057
- previous_loaded_from_memory=previous_loaded_from_memory,
1058
- mint_if_missing=True,
1059
- )
1118
+ error_store_id = None
1060
1119
  try:
1061
- await _persist_error_conversation(user_id, store_id, conversation_state)
1120
+ await persist_stream_state(response_id, mint_if_missing=True)
1121
+ error_store_id = store_id
1062
1122
  except Exception as store_error:
1063
1123
  logger.error(f"Failed to store error conversation: {store_error}")
1064
- yield _stream_error_chunk(display_text, store_id)
1124
+ yield _stream_error_chunk(display_text, error_store_id)
1125
+ finally:
1126
+ with anyio.CancelScope(shield=True):
1127
+ await upstream.aclose()
1065
1128
 
1066
1129
 
1067
1130
  async def delete_user_conversation(user_id: str, response_id: str) -> bool:
@@ -1075,31 +1138,18 @@ async def delete_user_conversation(user_id: str, response_id: str) -> bool:
1075
1138
  Exception — on unexpected storage errors so callers can distinguish
1076
1139
  a confirmed "not found" from a backend failure.
1077
1140
  """
1078
- deleted = False
1079
-
1080
- # Delete from in-memory storage (cannot raise)
1081
- if user_id in active_conversations and response_id in active_conversations[user_id]:
1082
- del active_conversations[user_id][response_id]
1083
- logger.debug(f"Deleted conversation {response_id} from memory for user {user_id}")
1084
- deleted = True
1141
+ from services.session_ownership_service import session_ownership_service
1085
1142
 
1086
- # Delete from persistent storage — let real errors propagate to the caller
1087
- conversation_deleted = await conversation_persistence.delete_conversation_thread(
1088
- user_id, response_id
1089
- )
1090
- if conversation_deleted:
1091
- logger.debug(
1092
- f"Deleted conversation {response_id} from persistent storage for user {user_id}"
1093
- )
1143
+ session_ids = await session_ownership_service.get_conversation_sessions(user_id, response_id)
1144
+ if not session_ids:
1145
+ return False
1146
+ canonical_id = session_ids[0]
1147
+ # Revoke ALL durable aliases before removing the local index. Failures must
1148
+ # reach the caller, not turn into a successful delete with reusable IDs.
1149
+ await session_ownership_service.release_conversation_sessions(user_id, session_ids)
1150
+ deleted = await conversation_persistence.delete_conversation_thread(user_id, canonical_id)
1151
+ if canonical_id in active_conversations.get(user_id, {}):
1152
+ del active_conversations[user_id][canonical_id]
1094
1153
  deleted = True
1095
1154
 
1096
- # Release session ownership (best-effort; never masks storage errors above)
1097
- try:
1098
- from services.session_ownership_service import session_ownership_service
1099
-
1100
- await session_ownership_service.release_session(user_id, response_id)
1101
- logger.debug(f"Released session ownership for {response_id} for user {user_id}")
1102
- except Exception as e:
1103
- logger.warning(f"Failed to release session ownership: {e}")
1104
-
1105
1155
  return deleted
@@ -1073,6 +1073,7 @@ async def _expand_selected_connector_files(
1073
1073
 
1074
1074
  async def _classify_connector_duplicates(
1075
1075
  connector,
1076
+ connector_type: str,
1076
1077
  selected_files_raw: list[Any],
1077
1078
  session_manager,
1078
1079
  user_id: str,
@@ -1081,6 +1082,14 @@ async def _classify_connector_duplicates(
1081
1082
  """Expand connector selections and split them into duplicate/non-duplicate files."""
1082
1083
  expanded_files_info = await _expand_selected_connector_files(connector, selected_files_raw)
1083
1084
  if not expanded_files_info:
1085
+ # The selection named only folders that expanded to nothing, so there is
1086
+ # nothing to compare. Counted against what the caller asked for, which
1087
+ # separates an empty expansion from an empty request.
1088
+ logger.info(
1089
+ "Duplicate check found nothing to check",
1090
+ connector_type=connector_type,
1091
+ selected=len(selected_files_raw),
1092
+ )
1084
1093
  return {
1085
1094
  "duplicate_names": [],
1086
1095
  "duplicate_files": [],
@@ -1101,6 +1110,13 @@ async def _classify_connector_duplicates(
1101
1110
  all_candidates.update(aliases)
1102
1111
 
1103
1112
  if not all_candidates:
1113
+ # Files expanded but none yielded a comparable name, so the index was
1114
+ # never queried: every file is reported new without having been checked.
1115
+ logger.info(
1116
+ "Duplicate check skipped: no comparable filenames",
1117
+ connector_type=connector_type,
1118
+ listed=len(cleaned_files),
1119
+ )
1104
1120
  return {
1105
1121
  "duplicate_names": [],
1106
1122
  "duplicate_files": [],
@@ -1131,6 +1147,16 @@ async def _classify_connector_duplicates(
1131
1147
  else:
1132
1148
  non_duplicate_files.append(file_info)
1133
1149
 
1150
+ # See the bucket classifier: logged so a "no dialog appeared" report can be
1151
+ # answered from the server side.
1152
+ logger.info(
1153
+ "Duplicate check complete",
1154
+ connector_type=connector_type,
1155
+ selected=len(cleaned_files),
1156
+ duplicates=len(duplicate_files),
1157
+ new_files=len(non_duplicate_files),
1158
+ )
1159
+
1134
1160
  return {
1135
1161
  "duplicate_names": list(dict.fromkeys(duplicate_names)),
1136
1162
  "duplicate_files": duplicate_files,
@@ -1335,6 +1361,11 @@ async def _classify_bucket_connector_duplicates(
1335
1361
  break
1336
1362
 
1337
1363
  if not all_files:
1364
+ logger.info(
1365
+ "Duplicate check found nothing to check",
1366
+ connector_type=connector_type,
1367
+ buckets=bucket_filter,
1368
+ )
1338
1369
  return {
1339
1370
  "duplicate_names": [],
1340
1371
  "duplicate_files": [],
@@ -1366,18 +1397,38 @@ async def _classify_bucket_connector_duplicates(
1366
1397
  duplicate_names: list[str] = []
1367
1398
  non_duplicate_files: list[dict[str, Any]] = []
1368
1399
  total = 0
1400
+ already_synced = 0
1369
1401
  for f in all_files:
1370
1402
  fid = f.get("id")
1371
1403
  if not fid:
1372
1404
  continue
1373
1405
  total += 1
1374
1406
  response_file = _connector_file_response(f, cleaned_name=_cleaned_blob_filename(f))
1375
- if fid in existing_set or fid in name_taken_ids:
1407
+ id_match = fid in existing_set
1408
+ if id_match:
1409
+ already_synced += 1
1410
+ if id_match or fid in name_taken_ids:
1376
1411
  duplicate_files.append(response_file)
1377
1412
  duplicate_names.append(response_file["name"])
1378
1413
  else:
1379
1414
  non_duplicate_files.append(response_file)
1380
1415
 
1416
+ # Both halves of the answer, so a "the dialog never appeared" report can be
1417
+ # settled from the logs: whether the listing came back, whether the names
1418
+ # were found in the index, and what the caller was told. Without it the only
1419
+ # way to tell a check that found nothing from one that never ran is the
1420
+ # browser's network tab.
1421
+ logger.info(
1422
+ "Duplicate check complete",
1423
+ connector_type=connector_type,
1424
+ buckets=bucket_filter,
1425
+ listed=total,
1426
+ already_synced_here=already_synced,
1427
+ name_taken_elsewhere=len(set(name_taken_ids) - existing_set),
1428
+ duplicates=len(duplicate_files),
1429
+ new_files=len(non_duplicate_files),
1430
+ )
1431
+
1381
1432
  return {
1382
1433
  "duplicate_names": list(dict.fromkeys(duplicate_names)),
1383
1434
  "duplicate_files": duplicate_files,
@@ -1402,7 +1453,19 @@ async def connector_check_duplicates(
1402
1453
 
1403
1454
  selected_files_raw = body.selected_files
1404
1455
  if not selected_files_raw and not body.bucket_filter:
1405
- return JSONResponse({"duplicate_names": []})
1456
+ # Returning an empty duplicate list here reads to the caller as "checked,
1457
+ # found nothing", and the UI goes on to ingest without ever offering the
1458
+ # overwrite choice — the same thing a broken check looks like. The
1459
+ # request asked about nothing, so say that instead of answering it.
1460
+ logger.warning(
1461
+ "Duplicate check asked about nothing",
1462
+ connector_type=connector_type,
1463
+ connection_id=body.connection_id,
1464
+ )
1465
+ return JSONResponse(
1466
+ {"error": "Specify selected_files or bucket_filter to check for duplicates"},
1467
+ status_code=400,
1468
+ )
1406
1469
 
1407
1470
  try:
1408
1471
  jwt_token = user.jwt_token
@@ -1458,6 +1521,7 @@ async def connector_check_duplicates(
1458
1521
  return JSONResponse(
1459
1522
  await _classify_connector_duplicates(
1460
1523
  connector=connector,
1524
+ connector_type=connector_type,
1461
1525
  selected_files_raw=selected_files_raw,
1462
1526
  session_manager=session_manager,
1463
1527
  user_id=user.user_id,
@@ -1786,6 +1850,7 @@ async def connector_sync(
1786
1850
  connector=await connector_service.get_connector(
1787
1851
  working_connection.connection_id
1788
1852
  ),
1853
+ connector_type=connector_type,
1789
1854
  selected_files_raw=file_infos,
1790
1855
  session_manager=session_manager,
1791
1856
  user_id=user.user_id,
@@ -2341,6 +2406,12 @@ async def connector_webhook(
2341
2406
  # shared=None like the other re-sync paths: no user is in the
2342
2407
  # loop to express an intent, so each file keeps the sharing state
2343
2408
  # it already has.
2409
+ # allow_anonymous_delete=False for the same reason: nobody is
2410
+ # here to authorize removing a document the whole instance can
2411
+ # see, so a webhook replaces only what the connection's owner
2412
+ # owns. A file indexed as shared is left as it is until someone
2413
+ # holding the permission syncs it; no connector that delivers
2414
+ # webhooks can index one today.
2344
2415
  task_id = await connector_service.sync_specific_files(
2345
2416
  connection.connection_id,
2346
2417
  connection.user_id,
@@ -2348,6 +2419,7 @@ async def connector_webhook(
2348
2419
  jwt_token=jwt_token,
2349
2420
  replace_duplicates=_connector_sync_should_replace(connector_type),
2350
2421
  shared=None,
2422
+ allow_anonymous_delete=False,
2351
2423
  )
2352
2424
 
2353
2425
  result = {