OpenRAG 0.2.5__tar.gz → 0.3.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 (153) hide show
  1. {openrag-0.2.5/src/openrag.egg-info → openrag-0.3.0}/PKG-INFO +14 -2
  2. {openrag-0.2.5 → openrag-0.3.0}/README.md +3 -1
  3. {openrag-0.2.5 → openrag-0.3.0}/pyproject.toml +13 -1
  4. {openrag-0.2.5 → openrag-0.3.0}/src/agent.py +98 -46
  5. {openrag-0.2.5 → openrag-0.3.0}/src/api/settings.py +142 -16
  6. {openrag-0.2.5 → openrag-0.3.0}/src/api/v1/chat.py +22 -0
  7. {openrag-0.2.5 → openrag-0.3.0}/src/config/settings.py +1 -0
  8. {openrag-0.2.5 → openrag-0.3.0}/src/dependencies.py +7 -3
  9. {openrag-0.2.5 → openrag-0.3.0}/src/main.py +9 -16
  10. {openrag-0.2.5 → openrag-0.3.0}/src/models/processors.py +0 -16
  11. {openrag-0.2.5 → openrag-0.3.0/src/openrag.egg-info}/PKG-INFO +14 -2
  12. {openrag-0.2.5 → openrag-0.3.0}/src/services/chat_service.py +7 -2
  13. {openrag-0.2.5 → openrag-0.3.0}/src/services/flows_service.py +95 -178
  14. {openrag-0.2.5 → openrag-0.3.0}/src/services/langflow_file_service.py +26 -23
  15. {openrag-0.2.5 → openrag-0.3.0}/src/services/langflow_history_service.py +10 -2
  16. {openrag-0.2.5 → openrag-0.3.0}/src/services/search_service.py +1 -1
  17. {openrag-0.2.5 → openrag-0.3.0}/src/session_manager.py +12 -16
  18. {openrag-0.2.5 → openrag-0.3.0}/src/tui/_assets/docker-compose.yml +8 -8
  19. {openrag-0.2.5 → openrag-0.3.0}/src/tui/_assets/flows/ingestion_flow.json +2379 -1811
  20. openrag-0.3.0/src/tui/_assets/flows/openrag_agent.json +5230 -0
  21. openrag-0.3.0/src/tui/_assets/flows/openrag_nudges.json +4851 -0
  22. {openrag-0.2.5 → openrag-0.3.0}/src/tui/_assets/flows/openrag_url_mcp.json +1766 -1454
  23. {openrag-0.2.5 → openrag-0.3.0}/src/tui/main.py +15 -2
  24. {openrag-0.2.5 → openrag-0.3.0}/src/tui/managers/container_manager.py +100 -137
  25. {openrag-0.2.5 → openrag-0.3.0}/src/tui/managers/env_manager.py +58 -14
  26. {openrag-0.2.5 → openrag-0.3.0}/src/tui/utils/startup_checks.py +86 -4
  27. {openrag-0.2.5 → openrag-0.3.0}/src/utils/container_utils.py +7 -2
  28. {openrag-0.2.5 → openrag-0.3.0}/src/utils/langflow_headers.py +2 -2
  29. openrag-0.2.5/src/tui/_assets/flows/openrag_agent.json +0 -4795
  30. openrag-0.2.5/src/tui/_assets/flows/openrag_nudges.json +0 -4122
  31. {openrag-0.2.5 → openrag-0.3.0}/LICENSE +0 -0
  32. {openrag-0.2.5 → openrag-0.3.0}/MANIFEST.in +0 -0
  33. {openrag-0.2.5 → openrag-0.3.0}/setup.cfg +0 -0
  34. {openrag-0.2.5 → openrag-0.3.0}/src/api/__init__.py +0 -0
  35. {openrag-0.2.5 → openrag-0.3.0}/src/api/auth.py +0 -0
  36. {openrag-0.2.5 → openrag-0.3.0}/src/api/chat.py +0 -0
  37. {openrag-0.2.5 → openrag-0.3.0}/src/api/connector_router.py +0 -0
  38. {openrag-0.2.5 → openrag-0.3.0}/src/api/connectors.py +0 -0
  39. {openrag-0.2.5 → openrag-0.3.0}/src/api/docling.py +0 -0
  40. {openrag-0.2.5 → openrag-0.3.0}/src/api/documents.py +0 -0
  41. {openrag-0.2.5 → openrag-0.3.0}/src/api/flows.py +0 -0
  42. {openrag-0.2.5 → openrag-0.3.0}/src/api/keys.py +0 -0
  43. {openrag-0.2.5 → openrag-0.3.0}/src/api/knowledge_filter.py +0 -0
  44. {openrag-0.2.5 → openrag-0.3.0}/src/api/langflow_files.py +0 -0
  45. {openrag-0.2.5 → openrag-0.3.0}/src/api/models.py +0 -0
  46. {openrag-0.2.5 → openrag-0.3.0}/src/api/nudges.py +0 -0
  47. {openrag-0.2.5 → openrag-0.3.0}/src/api/oidc.py +0 -0
  48. {openrag-0.2.5 → openrag-0.3.0}/src/api/provider_health.py +0 -0
  49. {openrag-0.2.5 → openrag-0.3.0}/src/api/provider_validation.py +0 -0
  50. {openrag-0.2.5 → openrag-0.3.0}/src/api/router.py +0 -0
  51. {openrag-0.2.5 → openrag-0.3.0}/src/api/search.py +0 -0
  52. {openrag-0.2.5 → openrag-0.3.0}/src/api/tasks.py +0 -0
  53. {openrag-0.2.5 → openrag-0.3.0}/src/api/upload.py +0 -0
  54. {openrag-0.2.5 → openrag-0.3.0}/src/api/v1/__init__.py +0 -0
  55. {openrag-0.2.5 → openrag-0.3.0}/src/api/v1/documents.py +0 -0
  56. {openrag-0.2.5 → openrag-0.3.0}/src/api/v1/knowledge_filters.py +0 -0
  57. {openrag-0.2.5 → openrag-0.3.0}/src/api/v1/models.py +0 -0
  58. {openrag-0.2.5 → openrag-0.3.0}/src/api/v1/search.py +0 -0
  59. {openrag-0.2.5 → openrag-0.3.0}/src/api/v1/settings.py +0 -0
  60. {openrag-0.2.5 → openrag-0.3.0}/src/auth_context.py +0 -0
  61. {openrag-0.2.5 → openrag-0.3.0}/src/config/__init__.py +0 -0
  62. {openrag-0.2.5 → openrag-0.3.0}/src/config/config_manager.py +0 -0
  63. {openrag-0.2.5 → openrag-0.3.0}/src/config/model_constants.py +0 -0
  64. {openrag-0.2.5 → openrag-0.3.0}/src/connectors/__init__.py +0 -0
  65. {openrag-0.2.5 → openrag-0.3.0}/src/connectors/base.py +0 -0
  66. {openrag-0.2.5 → openrag-0.3.0}/src/connectors/connection_manager.py +0 -0
  67. {openrag-0.2.5 → openrag-0.3.0}/src/connectors/google_drive/__init__.py +0 -0
  68. {openrag-0.2.5 → openrag-0.3.0}/src/connectors/google_drive/connector.py +0 -0
  69. {openrag-0.2.5 → openrag-0.3.0}/src/connectors/google_drive/oauth.py +0 -0
  70. {openrag-0.2.5 → openrag-0.3.0}/src/connectors/langflow_connector_service.py +0 -0
  71. {openrag-0.2.5 → openrag-0.3.0}/src/connectors/onedrive/__init__.py +0 -0
  72. {openrag-0.2.5 → openrag-0.3.0}/src/connectors/onedrive/connector.py +0 -0
  73. {openrag-0.2.5 → openrag-0.3.0}/src/connectors/onedrive/oauth.py +0 -0
  74. {openrag-0.2.5 → openrag-0.3.0}/src/connectors/service.py +0 -0
  75. {openrag-0.2.5 → openrag-0.3.0}/src/connectors/sharepoint/__init__.py +0 -0
  76. {openrag-0.2.5 → openrag-0.3.0}/src/connectors/sharepoint/connector.py +0 -0
  77. {openrag-0.2.5 → openrag-0.3.0}/src/connectors/sharepoint/oauth.py +0 -0
  78. {openrag-0.2.5 → openrag-0.3.0}/src/connectors/sharepoint/utils.py +0 -0
  79. {openrag-0.2.5 → openrag-0.3.0}/src/models/__init__.py +0 -0
  80. {openrag-0.2.5 → openrag-0.3.0}/src/models/tasks.py +0 -0
  81. {openrag-0.2.5 → openrag-0.3.0}/src/openrag.egg-info/SOURCES.txt +0 -0
  82. {openrag-0.2.5 → openrag-0.3.0}/src/openrag.egg-info/dependency_links.txt +0 -0
  83. {openrag-0.2.5 → openrag-0.3.0}/src/openrag.egg-info/entry_points.txt +0 -0
  84. {openrag-0.2.5 → openrag-0.3.0}/src/openrag.egg-info/requires.txt +0 -0
  85. {openrag-0.2.5 → openrag-0.3.0}/src/openrag.egg-info/top_level.txt +0 -0
  86. {openrag-0.2.5 → openrag-0.3.0}/src/services/__init__.py +0 -0
  87. {openrag-0.2.5 → openrag-0.3.0}/src/services/api_key_service.py +0 -0
  88. {openrag-0.2.5 → openrag-0.3.0}/src/services/auth_service.py +0 -0
  89. {openrag-0.2.5 → openrag-0.3.0}/src/services/conversation_persistence_service.py +0 -0
  90. {openrag-0.2.5 → openrag-0.3.0}/src/services/document_service.py +0 -0
  91. {openrag-0.2.5 → openrag-0.3.0}/src/services/knowledge_filter_service.py +0 -0
  92. {openrag-0.2.5 → openrag-0.3.0}/src/services/langflow_mcp_service.py +0 -0
  93. {openrag-0.2.5 → openrag-0.3.0}/src/services/models_service.py +0 -0
  94. {openrag-0.2.5 → openrag-0.3.0}/src/services/monitor_service.py +0 -0
  95. {openrag-0.2.5 → openrag-0.3.0}/src/services/session_ownership_service.py +0 -0
  96. {openrag-0.2.5 → openrag-0.3.0}/src/services/task_service.py +0 -0
  97. {openrag-0.2.5 → openrag-0.3.0}/src/tui/__init__.py +0 -0
  98. {openrag-0.2.5 → openrag-0.3.0}/src/tui/_assets/docker-compose.gpu.yml +0 -0
  99. {openrag-0.2.5 → openrag-0.3.0}/src/tui/_assets/flows/components/ollama_embedding.json +0 -0
  100. {openrag-0.2.5 → openrag-0.3.0}/src/tui/_assets/flows/components/ollama_llm.json +0 -0
  101. {openrag-0.2.5 → openrag-0.3.0}/src/tui/_assets/flows/components/ollama_llm_text.json +0 -0
  102. {openrag-0.2.5 → openrag-0.3.0}/src/tui/_assets/flows/components/watsonx_embedding.json +0 -0
  103. {openrag-0.2.5 → openrag-0.3.0}/src/tui/_assets/flows/components/watsonx_llm.json +0 -0
  104. {openrag-0.2.5 → openrag-0.3.0}/src/tui/_assets/flows/components/watsonx_llm_text.json +0 -0
  105. {openrag-0.2.5 → openrag-0.3.0}/src/tui/_assets/openrag-documents/docling.pdf +0 -0
  106. {openrag-0.2.5 → openrag-0.3.0}/src/tui/_assets/openrag-documents/ibm_anthropic.pdf +0 -0
  107. {openrag-0.2.5 → openrag-0.3.0}/src/tui/_assets/openrag-documents/openrag-documentation.pdf +0 -0
  108. {openrag-0.2.5 → openrag-0.3.0}/src/tui/_assets/openrag-documents/warmup_ocr.pdf +0 -0
  109. {openrag-0.2.5 → openrag-0.3.0}/src/tui/cli.py +0 -0
  110. {openrag-0.2.5 → openrag-0.3.0}/src/tui/config_fields.py +0 -0
  111. {openrag-0.2.5 → openrag-0.3.0}/src/tui/managers/__init__.py +0 -0
  112. {openrag-0.2.5 → openrag-0.3.0}/src/tui/managers/docling_manager.py +0 -0
  113. {openrag-0.2.5 → openrag-0.3.0}/src/tui/screens/__init__.py +0 -0
  114. {openrag-0.2.5 → openrag-0.3.0}/src/tui/screens/config.py +0 -0
  115. {openrag-0.2.5 → openrag-0.3.0}/src/tui/screens/diagnostics.py +0 -0
  116. {openrag-0.2.5 → openrag-0.3.0}/src/tui/screens/logs.py +0 -0
  117. {openrag-0.2.5 → openrag-0.3.0}/src/tui/screens/monitor.py +0 -0
  118. {openrag-0.2.5 → openrag-0.3.0}/src/tui/screens/welcome.py +0 -0
  119. {openrag-0.2.5 → openrag-0.3.0}/src/tui/utils/__init__.py +0 -0
  120. {openrag-0.2.5 → openrag-0.3.0}/src/tui/utils/clipboard.py +0 -0
  121. {openrag-0.2.5 → openrag-0.3.0}/src/tui/utils/platform.py +0 -0
  122. {openrag-0.2.5 → openrag-0.3.0}/src/tui/utils/validation.py +0 -0
  123. {openrag-0.2.5 → openrag-0.3.0}/src/tui/utils/version_check.py +0 -0
  124. {openrag-0.2.5 → openrag-0.3.0}/src/tui/widgets/__init__.py +0 -0
  125. {openrag-0.2.5 → openrag-0.3.0}/src/tui/widgets/command_modal.py +0 -0
  126. {openrag-0.2.5 → openrag-0.3.0}/src/tui/widgets/diagnostics_notification.py +0 -0
  127. {openrag-0.2.5 → openrag-0.3.0}/src/tui/widgets/error_notification.py +0 -0
  128. {openrag-0.2.5 → openrag-0.3.0}/src/tui/widgets/factory_reset_warning_modal.py +0 -0
  129. {openrag-0.2.5 → openrag-0.3.0}/src/tui/widgets/flow_backup_warning_modal.py +0 -0
  130. {openrag-0.2.5 → openrag-0.3.0}/src/tui/widgets/prune_options_modal.py +0 -0
  131. {openrag-0.2.5 → openrag-0.3.0}/src/tui/widgets/upgrade_instructions_modal.py +0 -0
  132. {openrag-0.2.5 → openrag-0.3.0}/src/tui/widgets/version_mismatch_warning_modal.py +0 -0
  133. {openrag-0.2.5 → openrag-0.3.0}/src/tui/widgets/waves.py +0 -0
  134. {openrag-0.2.5 → openrag-0.3.0}/src/utils/__init__.py +0 -0
  135. {openrag-0.2.5 → openrag-0.3.0}/src/utils/acl_utils.py +0 -0
  136. {openrag-0.2.5 → openrag-0.3.0}/src/utils/docling_client.py +0 -0
  137. {openrag-0.2.5 → openrag-0.3.0}/src/utils/document_processing.py +0 -0
  138. {openrag-0.2.5 → openrag-0.3.0}/src/utils/embedding_fields.py +0 -0
  139. {openrag-0.2.5 → openrag-0.3.0}/src/utils/embeddings.py +0 -0
  140. {openrag-0.2.5 → openrag-0.3.0}/src/utils/env_utils.py +0 -0
  141. {openrag-0.2.5 → openrag-0.3.0}/src/utils/file_utils.py +0 -0
  142. {openrag-0.2.5 → openrag-0.3.0}/src/utils/gpu_detection.py +0 -0
  143. {openrag-0.2.5 → openrag-0.3.0}/src/utils/hash_utils.py +0 -0
  144. {openrag-0.2.5 → openrag-0.3.0}/src/utils/langflow_utils.py +0 -0
  145. {openrag-0.2.5 → openrag-0.3.0}/src/utils/logging_config.py +0 -0
  146. {openrag-0.2.5 → openrag-0.3.0}/src/utils/opensearch_queries.py +0 -0
  147. {openrag-0.2.5 → openrag-0.3.0}/src/utils/opensearch_utils.py +0 -0
  148. {openrag-0.2.5 → openrag-0.3.0}/src/utils/paths.py +0 -0
  149. {openrag-0.2.5 → openrag-0.3.0}/src/utils/telemetry/__init__.py +0 -0
  150. {openrag-0.2.5 → openrag-0.3.0}/src/utils/telemetry/category.py +0 -0
  151. {openrag-0.2.5 → openrag-0.3.0}/src/utils/telemetry/client.py +0 -0
  152. {openrag-0.2.5 → openrag-0.3.0}/src/utils/telemetry/message_id.py +0 -0
  153. {openrag-0.2.5 → openrag-0.3.0}/src/utils/version_utils.py +0 -0
@@ -1,7 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openrag
3
- Version: 0.2.5
3
+ Version: 0.3.0
4
4
  Summary: OpenRAG is a comprehensive Retrieval-Augmented Generation platform that enables intelligent document search and AI-powered conversations.
5
+ Classifier: Development Status :: 4 - Beta
6
+ Classifier: Environment :: Console
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: Intended Audience :: Science/Research
9
+ Classifier: License :: OSI Approved :: Apache Software License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
5
15
  Requires-Python: >=3.13
6
16
  Description-Content-Type: text/markdown
7
17
  License-File: LICENSE
@@ -48,7 +58,7 @@ Dynamic: license-file
48
58
  [![GitHub stars](https://img.shields.io/github/stars/langflow-ai/openrag?style=social)](https://github.com/langflow-ai/openrag/stargazers)
49
59
  [![GitHub forks](https://img.shields.io/github/forks/langflow-ai/openrag?style=social)](https://github.com/langflow-ai/openrag/network/members)
50
60
 
51
- [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/langflow-ai/openrag)
61
+ [![Documentation](https://img.shields.io/badge/Documentation-773eff)](https://docs.openr.ag) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/langflow-ai/openrag)
52
62
 
53
63
  </div>
54
64
 
@@ -58,6 +68,8 @@ OpenRAG is a comprehensive Retrieval-Augmented Generation platform that enables
58
68
 
59
69
  Users can upload, process, and query documents through a chat interface backed by large language models and semantic search capabilities. The system utilizes Langflow for document ingestion, retrieval workflows, and intelligent nudges, providing a seamless RAG experience.
60
70
 
71
+ Check out the [documentation](https://docs.openr.ag/) or get started with the [quickstart](https://docs.openr.ag/quickstart).
72
+
61
73
  Built with [Starlette](https://github.com/Kludex/starlette) and [Next.js](https://github.com/vercel/next.js).
62
74
  Powered by [OpenSearch](https://github.com/opensearch-project/OpenSearch), [Langflow](https://github.com/langflow-ai/langflow), and [Docling](https://github.com/docling-project/docling).
63
75
 
@@ -18,7 +18,7 @@
18
18
  [![GitHub stars](https://img.shields.io/github/stars/langflow-ai/openrag?style=social)](https://github.com/langflow-ai/openrag/stargazers)
19
19
  [![GitHub forks](https://img.shields.io/github/forks/langflow-ai/openrag?style=social)](https://github.com/langflow-ai/openrag/network/members)
20
20
 
21
- [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/langflow-ai/openrag)
21
+ [![Documentation](https://img.shields.io/badge/Documentation-773eff)](https://docs.openr.ag) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/langflow-ai/openrag)
22
22
 
23
23
  </div>
24
24
 
@@ -28,6 +28,8 @@ OpenRAG is a comprehensive Retrieval-Augmented Generation platform that enables
28
28
 
29
29
  Users can upload, process, and query documents through a chat interface backed by large language models and semantic search capabilities. The system utilizes Langflow for document ingestion, retrieval workflows, and intelligent nudges, providing a seamless RAG experience.
30
30
 
31
+ Check out the [documentation](https://docs.openr.ag/) or get started with the [quickstart](https://docs.openr.ag/quickstart).
32
+
31
33
  Built with [Starlette](https://github.com/Kludex/starlette) and [Next.js](https://github.com/vercel/next.js).
32
34
  Powered by [OpenSearch](https://github.com/opensearch-project/OpenSearch), [Langflow](https://github.com/langflow-ai/langflow), and [Docling](https://github.com/docling-project/docling).
33
35
 
@@ -4,10 +4,22 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "openrag"
7
- version = "0.2.5"
7
+ version = "0.3.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"
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Environment :: Console",
14
+ "Intended Audience :: Developers",
15
+ "Intended Audience :: Science/Research",
16
+ "License :: OSI Approved :: Apache Software License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ ]
11
23
  dependencies = [
12
24
  "agentd>=0.2.2",
13
25
  "aiofiles>=24.1.0",
@@ -135,14 +135,13 @@ async def async_response_stream(
135
135
 
136
136
  full_response = ""
137
137
  chunk_count = 0
138
- detected_tool_call = False # Track if we've detected a tool call
138
+ detected_tool_call = False
139
139
  async for chunk in response:
140
140
  chunk_count += 1
141
141
  logger.debug(
142
142
  "Stream chunk received", chunk_count=chunk_count, chunk=str(chunk)
143
143
  )
144
144
 
145
- # Yield the raw event as JSON for the UI to process
146
145
  import json
147
146
 
148
147
  # Also extract text content for logging
@@ -186,7 +185,7 @@ async def async_response_stream(
186
185
  if isinstance(chunk_data, dict):
187
186
  # Check for any fields that might indicate tool usage
188
187
  potential_tool_fields = {
189
- k: v for k, v in chunk_data.items()
188
+ k: v for k, v in chunk_data.items()
190
189
  if any(keyword in str(k).lower() for keyword in ['tool', 'call', 'retrieval', 'function', 'result', 'output'])
191
190
  }
192
191
  if potential_tool_fields:
@@ -235,8 +234,8 @@ async def async_response_stream(
235
234
  "tool_name": "Retrieval",
236
235
  "status": "completed",
237
236
  "inputs": {"implicit": True, "backend_detected": True},
238
- "results": chunk_data.get('results') or chunk_data.get('outputs') or
239
- chunk_data.get('retrieved_documents') or
237
+ "results": chunk_data.get('results') or chunk_data.get('outputs') or
238
+ chunk_data.get('retrieved_documents') or
240
239
  chunk_data.get('retrieval_results') or []
241
240
  }
242
241
  }
@@ -625,8 +624,25 @@ async def async_langflow_chat(
625
624
  message_count=len(conversation_state["messages"]),
626
625
  )
627
626
 
627
+ # Extract sources from retrieval tool calls in the response
628
+ sources = []
629
+ if hasattr(response_obj, "output") and response_obj.output:
630
+ for output_item in response_obj.output:
631
+ item_type = getattr(output_item, "type", None)
632
+ if item_type in ("tool_call", "retrieval_call"):
633
+ for result in getattr(output_item, "results", None) or []:
634
+ rd = result.model_dump() if hasattr(result, "model_dump") else (result if isinstance(result, dict) else {})
635
+ if "text" in rd:
636
+ sources.append({
637
+ "filename": rd.get("filename", ""),
638
+ "text": rd.get("text", ""),
639
+ "score": rd.get("score", 0),
640
+ "page": rd.get("page"),
641
+ "mimetype": rd.get("mimetype"),
642
+ })
643
+
628
644
  if not store_conversation:
629
- return response_text, response_id
645
+ return response_text, response_id, sources
630
646
 
631
647
  # Store the conversation thread with its response_id
632
648
  if response_id:
@@ -662,7 +678,7 @@ async def async_langflow_chat(
662
678
  else:
663
679
  logger.warning("No response_id received from langflow, conversation not stored")
664
680
 
665
- return response_text, response_id
681
+ return response_text, response_id, sources
666
682
 
667
683
 
668
684
  # Async langflow function with conversation storage (streaming)
@@ -698,47 +714,55 @@ async def async_langflow_chat_stream(
698
714
  response_id = None
699
715
  usage_data = None
700
716
  collected_chunks = [] # Store all chunks for function call data
717
+ error_occurred = False
701
718
 
702
- async for chunk in async_stream(
703
- langflow_client,
704
- prompt,
705
- flow_id,
706
- extra_headers=extra_headers,
707
- previous_response_id=previous_response_id,
708
- log_prefix="langflow",
709
- ):
710
- # Extract text content to build full response for history
711
- try:
712
- import json
713
-
714
- chunk_data = json.loads(chunk.decode("utf-8"))
715
- collected_chunks.append(chunk_data) # Collect all chunk data
716
-
717
- if "delta" in chunk_data and "content" in chunk_data["delta"]:
718
- full_response += chunk_data["delta"]["content"]
719
- # Extract response_id from chunk
720
- if "id" in chunk_data:
721
- response_id = chunk_data["id"]
722
- elif "response_id" in chunk_data:
723
- response_id = chunk_data["response_id"]
724
- # Capture usage from response.completed event
725
- if chunk_data.get("type") == "response.completed":
726
- response_obj = chunk_data.get("response", {})
727
- usage_data = response_obj.get("usage")
728
- except:
729
- pass
730
- yield chunk
719
+ try:
720
+ async for chunk in async_stream(
721
+ langflow_client,
722
+ prompt,
723
+ flow_id,
724
+ extra_headers=extra_headers,
725
+ previous_response_id=previous_response_id,
726
+ log_prefix="langflow",
727
+ ):
728
+ # Extract text content to build full response for history
729
+ try:
730
+ import json
731
+
732
+ chunk_data = json.loads(chunk.decode("utf-8"))
733
+ collected_chunks.append(chunk_data) # Collect all chunk data
734
+
735
+ if "delta" in chunk_data and "content" in chunk_data["delta"]:
736
+ full_response += chunk_data["delta"]["content"]
737
+ # Extract response_id from chunk
738
+ if "id" in chunk_data:
739
+ response_id = chunk_data["id"]
740
+ elif "response_id" in chunk_data:
741
+ response_id = chunk_data["response_id"]
742
+
743
+ # Check for error status
744
+ if chunk_data.get("finish_reason") == "error" or chunk_data.get("status") == "failed":
745
+ error_occurred = True
746
+ logger.error("Error detected in Langflow stream chunk")
747
+ # Capture usage from response.completed event
748
+ if chunk_data.get("type") == "response.completed":
749
+ response_obj = chunk_data.get("response", {})
750
+ usage_data = response_obj.get("usage")
751
+ except:
752
+ pass
753
+ yield chunk
731
754
 
732
- # Add the complete assistant response to message history with response_id, timestamp, and function call data
733
- if full_response:
734
- assistant_message = {
735
- "role": "assistant",
736
- "content": full_response,
737
- "response_id": response_id,
738
- "timestamp": datetime.now(),
739
- "chunks": collected_chunks, # Store complete chunk data for function calls
740
- }
741
- # Store usage data if available (from response.completed event)
755
+ # Add the complete assistant response to message history with response_id, timestamp, and function call data
756
+ if full_response:
757
+ assistant_message = {
758
+ "role": "assistant",
759
+ "content": full_response,
760
+ "response_id": response_id,
761
+ "timestamp": datetime.now(),
762
+ "chunks": collected_chunks, # Store complete chunk data for function calls
763
+ "error": error_occurred, # Mark if this was an error response
764
+ }
765
+ # Store usage data if available (from response.completed event)
742
766
  if usage_data:
743
767
  assistant_message["response_data"] = {"usage": usage_data}
744
768
  conversation_state["messages"].append(assistant_message)
@@ -760,6 +784,34 @@ async def async_langflow_chat_stream(
760
784
  logger.debug(
761
785
  f"Stored langflow conversation thread for user {user_id} with response_id: {response_id}"
762
786
  )
787
+ except Exception as e:
788
+ # Log the error
789
+ logger.error(f"Error in langflow chat stream: {e}", exc_info=True)
790
+ error_occurred = True
791
+
792
+ # Store error message in conversation history so it persists
793
+ error_message = {
794
+ "role": "assistant",
795
+ "content": f"Sorry, I encountered an error: {str(e)}",
796
+ "timestamp": datetime.now(),
797
+ "error": True,
798
+ }
799
+ conversation_state["messages"].append(error_message)
800
+
801
+ # Try to store the conversation with error message
802
+ # Use a temporary response_id if we don't have one
803
+ if not response_id:
804
+ response_id = f"error_{user_id}_{int(datetime.now().timestamp())}"
805
+
806
+ try:
807
+ conversation_state["last_activity"] = datetime.now()
808
+ await store_conversation_thread(user_id, response_id, conversation_state)
809
+ logger.debug(f"Stored conversation with error for user {user_id}")
810
+ except Exception as store_error:
811
+ logger.error(f"Failed to store error conversation: {store_error}")
812
+
813
+ # Re-raise the exception so it propagates to the API layer
814
+ raise
763
815
 
764
816
 
765
817
  async def delete_user_conversation(user_id: str, response_id: str) -> bool:
@@ -55,6 +55,10 @@ class SettingsUpdateBody(BaseModel):
55
55
  watsonx_endpoint: Optional[str] = Field(None, min_length=1)
56
56
  watsonx_project_id: Optional[str] = Field(None, min_length=1)
57
57
  ollama_endpoint: Optional[str] = Field(None, min_length=1)
58
+ remove_ollama_config: Optional[bool] = None
59
+ remove_openai_config: Optional[bool] = None
60
+ remove_anthropic_config: Optional[bool] = None
61
+ remove_watsonx_config: Optional[bool] = None
58
62
 
59
63
 
60
64
  class OnboardingBody(BaseModel):
@@ -168,6 +172,7 @@ class OnboardingResponse(BaseModel):
168
172
  edited: bool
169
173
  sample_data_ingested: bool
170
174
  openrag_docs_filter_id: Optional[str] = None
175
+ task_id: Optional[str] = None
171
176
 
172
177
  class DoclingConfig(BaseModel):
173
178
  do_ocr: bool
@@ -360,6 +365,22 @@ async def get_settings(
360
365
  )
361
366
 
362
367
 
368
+ def _first_configured_llm_provider(config, excluding: str) -> str:
369
+ """Return the first configured LLM provider that isn't `excluding`."""
370
+ for p in ["openai", "anthropic", "watsonx", "ollama"]:
371
+ if p != excluding and getattr(config.providers, p).configured:
372
+ return p
373
+ return "openai"
374
+
375
+
376
+ def _first_configured_embedding_provider(config, excluding: str) -> str:
377
+ """Return the first configured embedding provider (openai/watsonx/ollama) that isn't `excluding`."""
378
+ for p in ["openai", "watsonx", "ollama"]:
379
+ if p != excluding and getattr(config.providers, p).configured:
380
+ return p
381
+ return "openai"
382
+
383
+
363
384
  async def update_settings(
364
385
  body: SettingsUpdateBody,
365
386
  session_manager=Depends(get_session_manager),
@@ -672,6 +693,99 @@ async def update_settings(
672
693
  config_updated = True
673
694
  provider_updated = True
674
695
 
696
+ if body.remove_ollama_config:
697
+ other_providers_configured = (
698
+ current_config.providers.openai.configured
699
+ or current_config.providers.anthropic.configured
700
+ or current_config.providers.watsonx.configured
701
+ )
702
+ if not other_providers_configured:
703
+ return JSONResponse(
704
+ {"error": "Cannot remove Ollama configuration: configure another model provider first."},
705
+ status_code=400,
706
+ )
707
+ current_config.providers.ollama.endpoint = ""
708
+ current_config.providers.ollama.configured = False
709
+ if current_config.agent.llm_provider == "ollama":
710
+ current_config.agent.llm_provider = _first_configured_llm_provider(current_config, "ollama")
711
+ current_config.agent.llm_model = ""
712
+ if current_config.knowledge.embedding_provider == "ollama":
713
+ current_config.knowledge.embedding_provider = _first_configured_embedding_provider(current_config, "ollama")
714
+ current_config.knowledge.embedding_model = ""
715
+ config_updated = True
716
+ provider_updated = True
717
+
718
+ if body.remove_openai_config:
719
+ other_providers_configured = (
720
+ current_config.providers.anthropic.configured
721
+ or current_config.providers.watsonx.configured
722
+ or current_config.providers.ollama.configured
723
+ )
724
+ if not other_providers_configured:
725
+ return JSONResponse(
726
+ {"error": "Cannot remove OpenAI configuration: configure another model provider first."},
727
+ status_code=400,
728
+ )
729
+ current_config.providers.openai.api_key = ""
730
+ current_config.providers.openai.configured = False
731
+ if current_config.agent.llm_provider == "openai":
732
+ fb = _first_configured_llm_provider(current_config, "openai")
733
+ current_config.agent.llm_provider = fb
734
+ current_config.agent.llm_model = ""
735
+ if current_config.knowledge.embedding_provider == "openai":
736
+ fb = _first_configured_embedding_provider(current_config, "openai")
737
+ current_config.knowledge.embedding_provider = fb
738
+ current_config.knowledge.embedding_model = ""
739
+ config_updated = True
740
+ provider_updated = True
741
+
742
+ if body.remove_anthropic_config:
743
+ other_providers_configured = (
744
+ current_config.providers.openai.configured
745
+ or current_config.providers.watsonx.configured
746
+ or current_config.providers.ollama.configured
747
+ )
748
+ if not other_providers_configured:
749
+ return JSONResponse(
750
+ {"error": "Cannot remove Anthropic configuration: configure another model provider first."},
751
+ status_code=400,
752
+ )
753
+ current_config.providers.anthropic.api_key = ""
754
+ current_config.providers.anthropic.configured = False
755
+ if current_config.agent.llm_provider == "anthropic":
756
+ fb = _first_configured_llm_provider(current_config, "anthropic")
757
+ current_config.agent.llm_provider = fb
758
+ current_config.agent.llm_model = ""
759
+ # Anthropic is not a valid embedding provider; no embedding reset needed
760
+ config_updated = True
761
+ provider_updated = True
762
+
763
+ if body.remove_watsonx_config:
764
+ other_providers_configured = (
765
+ current_config.providers.openai.configured
766
+ or current_config.providers.anthropic.configured
767
+ or current_config.providers.ollama.configured
768
+ )
769
+ if not other_providers_configured:
770
+ return JSONResponse(
771
+ {"error": "Cannot remove IBM watsonx.ai configuration: configure another model provider first."},
772
+ status_code=400,
773
+ )
774
+ current_config.providers.watsonx.api_key = ""
775
+ current_config.providers.watsonx.endpoint = ""
776
+ current_config.providers.watsonx.project_id = ""
777
+ current_config.providers.watsonx.configured = False
778
+ if current_config.agent.llm_provider == "watsonx":
779
+ fb = _first_configured_llm_provider(current_config, "watsonx")
780
+ current_config.agent.llm_provider = fb
781
+ current_config.agent.llm_model = ""
782
+ if current_config.knowledge.embedding_provider == "watsonx":
783
+ fb = _first_configured_embedding_provider(current_config, "watsonx")
784
+ current_config.knowledge.embedding_provider = fb
785
+ current_config.knowledge.embedding_model = ""
786
+ config_updated = True
787
+ provider_updated = True
788
+
675
789
  if provider_updated:
676
790
  await TelemetryClient.send_event(
677
791
  Category.SETTINGS_OPERATIONS,
@@ -692,7 +806,7 @@ async def update_settings(
692
806
  # Update Langflow global variables and model values if provider settings changed
693
807
  await clients.refresh_patched_client()
694
808
 
695
- if should_validate:
809
+ if should_validate or provider_updated:
696
810
  try:
697
811
  flows_service = _get_flows_service()
698
812
 
@@ -705,8 +819,8 @@ async def update_settings(
705
819
  current_config, session_manager
706
820
  )
707
821
 
708
- # Update model values if provider or model changed
709
- 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:
822
+ # Update model values if provider or model changed (including removals that trigger fallback)
823
+ 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:
710
824
  await _update_langflow_model_values(current_config, flows_service)
711
825
 
712
826
  except Exception as e:
@@ -975,6 +1089,8 @@ async def onboarding(
975
1089
  )
976
1090
  raise
977
1091
 
1092
+ task_id = None
1093
+
978
1094
  # Initialize the OpenSearch index if embedding model is configured
979
1095
  if body.embedding_model or body.embedding_provider:
980
1096
  try:
@@ -1004,7 +1120,7 @@ async def onboarding(
1004
1120
  # Import the function here to avoid circular imports
1005
1121
  from main import ingest_default_documents_when_ready
1006
1122
 
1007
- await ingest_default_documents_when_ready(
1123
+ task_id = await ingest_default_documents_when_ready(
1008
1124
  document_service,
1009
1125
  task_service,
1010
1126
  langflow_file_service,
@@ -1094,6 +1210,7 @@ async def onboarding(
1094
1210
  edited=True, # Confirm that config is now marked as edited
1095
1211
  sample_data_ingested=should_ingest_sample_data,
1096
1212
  openrag_docs_filter_id=openrag_docs_filter_id,
1213
+ task_id=task_id,
1097
1214
  )
1098
1215
 
1099
1216
  except Exception as e:
@@ -1121,7 +1238,7 @@ async def _create_openrag_docs_filter(
1121
1238
  return None
1122
1239
 
1123
1240
  # Get JWT token
1124
- jwt_token = user.jwt_token
1241
+ jwt_token = user.jwt_token
1125
1242
 
1126
1243
  # In no-auth mode, set owner to None so filter is visible to all users
1127
1244
  # In auth mode, use the actual user as owner
@@ -1192,6 +1309,12 @@ async def _update_langflow_global_variables(config):
1192
1309
  )
1193
1310
  logger.info("Set WATSONX_PROJECT_ID global variable in Langflow")
1194
1311
 
1312
+ if config.providers.watsonx.endpoint:
1313
+ await clients._create_langflow_global_variable(
1314
+ "WATSONX_URL", config.providers.watsonx.endpoint, modify=True
1315
+ )
1316
+ logger.info("Set WATSONX_URL global variable in Langflow")
1317
+
1195
1318
  # OpenAI global variables
1196
1319
  if config.providers.openai.api_key:
1197
1320
  await clients._create_langflow_global_variable(
@@ -1208,11 +1331,19 @@ async def _update_langflow_global_variables(config):
1208
1331
 
1209
1332
  # Ollama global variables
1210
1333
  if config.providers.ollama.endpoint:
1211
- endpoint = transform_localhost_url(config.providers.ollama.endpoint)
1212
- await clients._create_langflow_global_variable(
1213
- "OLLAMA_BASE_URL", endpoint, modify=True
1214
- )
1215
- logger.info("Set OLLAMA_BASE_URL global variable in Langflow")
1334
+
1335
+ try:
1336
+ endpoint = transform_localhost_url(config.providers.ollama.endpoint, is_langflow=True, is_podman=True)
1337
+ await clients._create_langflow_global_variable(
1338
+ "OLLAMA_BASE_URL", endpoint, modify=True
1339
+ )
1340
+ logger.info("Set OLLAMA_BASE_URL global variable in Langflow (Podman)")
1341
+ except Exception:
1342
+ endpoint = transform_localhost_url(config.providers.ollama.endpoint, is_langflow=True, is_podman=False)
1343
+ await clients._create_langflow_global_variable(
1344
+ "OLLAMA_BASE_URL", endpoint, modify=True
1345
+ )
1346
+ logger.info("Set OLLAMA_BASE_URL global variable in Langflow (Docker)")
1216
1347
 
1217
1348
  if config.knowledge.embedding_model:
1218
1349
  await clients._create_langflow_global_variable(
@@ -1269,13 +1400,10 @@ async def _update_langflow_model_values(config, flows_service):
1269
1400
  try:
1270
1401
  # Update LLM model values
1271
1402
  llm_provider = config.agent.llm_provider.lower()
1272
- llm_provider_config = config.get_llm_provider_config()
1273
- llm_endpoint = getattr(llm_provider_config, "endpoint", None)
1274
1403
 
1275
1404
  await flows_service.change_langflow_model_value(
1276
1405
  llm_provider,
1277
1406
  llm_model=config.agent.llm_model,
1278
- endpoint=llm_endpoint,
1279
1407
  )
1280
1408
  logger.info(
1281
1409
  f"Successfully updated Langflow flows for LLM provider {llm_provider}"
@@ -1283,13 +1411,10 @@ async def _update_langflow_model_values(config, flows_service):
1283
1411
 
1284
1412
  # Update embedding model values
1285
1413
  embedding_provider = config.knowledge.embedding_provider.lower()
1286
- embedding_provider_config = config.get_embedding_provider_config()
1287
- embedding_endpoint = getattr(embedding_provider_config, "endpoint", None)
1288
1414
 
1289
1415
  await flows_service.change_langflow_model_value(
1290
1416
  embedding_provider,
1291
1417
  embedding_model=config.knowledge.embedding_model,
1292
- endpoint=embedding_endpoint,
1293
1418
  )
1294
1419
  logger.info(
1295
1420
  f"Successfully updated Langflow flows for embedding provider {embedding_provider}"
@@ -1517,6 +1642,7 @@ async def rollback_onboarding(
1517
1642
 
1518
1643
  # Mark config as not edited so user can go through onboarding again
1519
1644
  current_config.edited = False
1645
+ current_config.onboarding.current_step = 0
1520
1646
 
1521
1647
  # Save the rolled back configuration manually to avoid save_config_file setting edited=True
1522
1648
  try:
@@ -28,6 +28,21 @@ class ChatV1Body(BaseModel):
28
28
  filter_id: Optional[str] = None
29
29
 
30
30
 
31
+ def _extract_sources(item: dict) -> list[dict]:
32
+ """Extract sources from a retrieval tool call item."""
33
+ sources = []
34
+ for result in item.get("results", []):
35
+ if isinstance(result, dict) and "text" in result:
36
+ sources.append({
37
+ "filename": result.get("filename", ""),
38
+ "text": result.get("text", ""),
39
+ "score": result.get("score", 0),
40
+ "page": result.get("page"),
41
+ "mimetype": result.get("mimetype"),
42
+ })
43
+ return sources
44
+
45
+
31
46
  async def _transform_stream_to_sse(raw_stream, chat_id_container: dict):
32
47
  """Transform raw Langflow streaming format to clean SSE events for v1 API."""
33
48
  full_text = ""
@@ -64,6 +79,13 @@ async def _transform_stream_to_sse(raw_stream, chat_id_container: dict):
64
79
  full_text += delta_text
65
80
  yield f"data: {json.dumps({'type': 'content', 'delta': delta_text})}\n\n"
66
81
 
82
+ # Emit sources from retrieval tool calls
83
+ item = chunk_data.get("item", {})
84
+ if item.get("type") in ("retrieval_call", "tool_call") and item.get("results"):
85
+ sources = _extract_sources(item)
86
+ if sources:
87
+ yield f"data: {json.dumps({'type': 'sources', 'sources': sources})}\n\n"
88
+
67
89
  if not chat_id:
68
90
  chat_id = chunk_data.get("id") or chunk_data.get("response_id")
69
91
 
@@ -697,6 +697,7 @@ class AppClients:
697
697
  variable_name=name,
698
698
  error=str(e),
699
699
  )
700
+ raise e
700
701
 
701
702
  async def _update_langflow_global_variable(self, name: str, value: str):
702
703
  """Update an existing global variable in Langflow via API"""
@@ -162,6 +162,7 @@ def get_optional_user(
162
162
  async def get_api_key_user_async(
163
163
  request: Request,
164
164
  api_key_service=Depends(get_api_key_service),
165
+ session_manager=Depends(get_session_manager),
165
166
  ) -> User:
166
167
  """
167
168
  Async dependency: require API key authentication.
@@ -208,9 +209,12 @@ async def get_api_key_user_async(
208
209
  provider="api_key",
209
210
  )
210
211
 
211
- # API Key users don't typically have a JWT for OpenSearch OIDC,
212
- # but we can try to get an effective one if needed
213
- user_with_token = dataclasses.replace(user, jwt_token=None)
212
+ # Register the API key user so get_effective_jwt_token can find them
213
+ if user.user_id not in session_manager.users:
214
+ session_manager.users[user.user_id] = user
215
+
216
+ effective_token = session_manager.get_effective_jwt_token(user.user_id, None)
217
+ user_with_token = dataclasses.replace(user, jwt_token=effective_token)
214
218
 
215
219
  request.state.user = user_with_token
216
220
  request.state.api_key_id = user_info["key_id"]