atlas-chat 0.1.0__py3-none-any.whl

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 (250) hide show
  1. atlas/__init__.py +40 -0
  2. atlas/application/__init__.py +7 -0
  3. atlas/application/chat/__init__.py +7 -0
  4. atlas/application/chat/agent/__init__.py +10 -0
  5. atlas/application/chat/agent/act_loop.py +179 -0
  6. atlas/application/chat/agent/factory.py +142 -0
  7. atlas/application/chat/agent/protocols.py +46 -0
  8. atlas/application/chat/agent/react_loop.py +338 -0
  9. atlas/application/chat/agent/think_act_loop.py +171 -0
  10. atlas/application/chat/approval_manager.py +151 -0
  11. atlas/application/chat/elicitation_manager.py +191 -0
  12. atlas/application/chat/events/__init__.py +1 -0
  13. atlas/application/chat/events/agent_event_relay.py +112 -0
  14. atlas/application/chat/modes/__init__.py +1 -0
  15. atlas/application/chat/modes/agent.py +125 -0
  16. atlas/application/chat/modes/plain.py +74 -0
  17. atlas/application/chat/modes/rag.py +81 -0
  18. atlas/application/chat/modes/tools.py +179 -0
  19. atlas/application/chat/orchestrator.py +213 -0
  20. atlas/application/chat/policies/__init__.py +1 -0
  21. atlas/application/chat/policies/tool_authorization.py +99 -0
  22. atlas/application/chat/preprocessors/__init__.py +1 -0
  23. atlas/application/chat/preprocessors/message_builder.py +92 -0
  24. atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
  25. atlas/application/chat/service.py +454 -0
  26. atlas/application/chat/utilities/__init__.py +6 -0
  27. atlas/application/chat/utilities/error_handler.py +367 -0
  28. atlas/application/chat/utilities/event_notifier.py +546 -0
  29. atlas/application/chat/utilities/file_processor.py +613 -0
  30. atlas/application/chat/utilities/tool_executor.py +789 -0
  31. atlas/atlas_chat_cli.py +347 -0
  32. atlas/atlas_client.py +238 -0
  33. atlas/core/__init__.py +0 -0
  34. atlas/core/auth.py +205 -0
  35. atlas/core/authorization_manager.py +27 -0
  36. atlas/core/capabilities.py +123 -0
  37. atlas/core/compliance.py +215 -0
  38. atlas/core/domain_whitelist.py +147 -0
  39. atlas/core/domain_whitelist_middleware.py +82 -0
  40. atlas/core/http_client.py +28 -0
  41. atlas/core/log_sanitizer.py +102 -0
  42. atlas/core/metrics_logger.py +59 -0
  43. atlas/core/middleware.py +131 -0
  44. atlas/core/otel_config.py +242 -0
  45. atlas/core/prompt_risk.py +200 -0
  46. atlas/core/rate_limit.py +0 -0
  47. atlas/core/rate_limit_middleware.py +64 -0
  48. atlas/core/security_headers_middleware.py +51 -0
  49. atlas/domain/__init__.py +37 -0
  50. atlas/domain/chat/__init__.py +1 -0
  51. atlas/domain/chat/dtos.py +85 -0
  52. atlas/domain/errors.py +96 -0
  53. atlas/domain/messages/__init__.py +12 -0
  54. atlas/domain/messages/models.py +160 -0
  55. atlas/domain/rag_mcp_service.py +664 -0
  56. atlas/domain/sessions/__init__.py +7 -0
  57. atlas/domain/sessions/models.py +36 -0
  58. atlas/domain/unified_rag_service.py +371 -0
  59. atlas/infrastructure/__init__.py +10 -0
  60. atlas/infrastructure/app_factory.py +135 -0
  61. atlas/infrastructure/events/__init__.py +1 -0
  62. atlas/infrastructure/events/cli_event_publisher.py +140 -0
  63. atlas/infrastructure/events/websocket_publisher.py +140 -0
  64. atlas/infrastructure/sessions/in_memory_repository.py +56 -0
  65. atlas/infrastructure/transport/__init__.py +7 -0
  66. atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
  67. atlas/init_cli.py +226 -0
  68. atlas/interfaces/__init__.py +15 -0
  69. atlas/interfaces/events.py +134 -0
  70. atlas/interfaces/llm.py +54 -0
  71. atlas/interfaces/rag.py +40 -0
  72. atlas/interfaces/sessions.py +75 -0
  73. atlas/interfaces/tools.py +57 -0
  74. atlas/interfaces/transport.py +24 -0
  75. atlas/main.py +564 -0
  76. atlas/mcp/api_key_demo/README.md +76 -0
  77. atlas/mcp/api_key_demo/main.py +172 -0
  78. atlas/mcp/api_key_demo/run.sh +56 -0
  79. atlas/mcp/basictable/main.py +147 -0
  80. atlas/mcp/calculator/main.py +149 -0
  81. atlas/mcp/code-executor/execution_engine.py +98 -0
  82. atlas/mcp/code-executor/execution_environment.py +95 -0
  83. atlas/mcp/code-executor/main.py +528 -0
  84. atlas/mcp/code-executor/result_processing.py +276 -0
  85. atlas/mcp/code-executor/script_generation.py +195 -0
  86. atlas/mcp/code-executor/security_checker.py +140 -0
  87. atlas/mcp/corporate_cars/main.py +437 -0
  88. atlas/mcp/csv_reporter/main.py +545 -0
  89. atlas/mcp/duckduckgo/main.py +182 -0
  90. atlas/mcp/elicitation_demo/README.md +171 -0
  91. atlas/mcp/elicitation_demo/main.py +262 -0
  92. atlas/mcp/env-demo/README.md +158 -0
  93. atlas/mcp/env-demo/main.py +199 -0
  94. atlas/mcp/file_size_test/main.py +284 -0
  95. atlas/mcp/filesystem/main.py +348 -0
  96. atlas/mcp/image_demo/main.py +113 -0
  97. atlas/mcp/image_demo/requirements.txt +4 -0
  98. atlas/mcp/logging_demo/README.md +72 -0
  99. atlas/mcp/logging_demo/main.py +103 -0
  100. atlas/mcp/many_tools_demo/main.py +50 -0
  101. atlas/mcp/order_database/__init__.py +0 -0
  102. atlas/mcp/order_database/main.py +369 -0
  103. atlas/mcp/order_database/signal_data.csv +1001 -0
  104. atlas/mcp/pdfbasic/main.py +394 -0
  105. atlas/mcp/pptx_generator/main.py +760 -0
  106. atlas/mcp/pptx_generator/requirements.txt +13 -0
  107. atlas/mcp/pptx_generator/run_test.sh +1 -0
  108. atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
  109. atlas/mcp/progress_demo/main.py +167 -0
  110. atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
  111. atlas/mcp/progress_updates_demo/README.md +120 -0
  112. atlas/mcp/progress_updates_demo/main.py +497 -0
  113. atlas/mcp/prompts/main.py +222 -0
  114. atlas/mcp/public_demo/main.py +189 -0
  115. atlas/mcp/sampling_demo/README.md +169 -0
  116. atlas/mcp/sampling_demo/main.py +234 -0
  117. atlas/mcp/thinking/main.py +77 -0
  118. atlas/mcp/tool_planner/main.py +240 -0
  119. atlas/mcp/ui-demo/badmesh.png +0 -0
  120. atlas/mcp/ui-demo/main.py +383 -0
  121. atlas/mcp/ui-demo/templates/button_demo.html +32 -0
  122. atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
  123. atlas/mcp/ui-demo/templates/form_demo.html +28 -0
  124. atlas/mcp/username-override-demo/README.md +320 -0
  125. atlas/mcp/username-override-demo/main.py +308 -0
  126. atlas/modules/__init__.py +0 -0
  127. atlas/modules/config/__init__.py +34 -0
  128. atlas/modules/config/cli.py +231 -0
  129. atlas/modules/config/config_manager.py +1096 -0
  130. atlas/modules/file_storage/__init__.py +22 -0
  131. atlas/modules/file_storage/cli.py +330 -0
  132. atlas/modules/file_storage/content_extractor.py +290 -0
  133. atlas/modules/file_storage/manager.py +295 -0
  134. atlas/modules/file_storage/mock_s3_client.py +402 -0
  135. atlas/modules/file_storage/s3_client.py +417 -0
  136. atlas/modules/llm/__init__.py +19 -0
  137. atlas/modules/llm/caller.py +287 -0
  138. atlas/modules/llm/litellm_caller.py +675 -0
  139. atlas/modules/llm/models.py +19 -0
  140. atlas/modules/mcp_tools/__init__.py +17 -0
  141. atlas/modules/mcp_tools/client.py +2123 -0
  142. atlas/modules/mcp_tools/token_storage.py +556 -0
  143. atlas/modules/prompts/prompt_provider.py +130 -0
  144. atlas/modules/rag/__init__.py +24 -0
  145. atlas/modules/rag/atlas_rag_client.py +336 -0
  146. atlas/modules/rag/client.py +129 -0
  147. atlas/routes/admin_routes.py +865 -0
  148. atlas/routes/config_routes.py +484 -0
  149. atlas/routes/feedback_routes.py +361 -0
  150. atlas/routes/files_routes.py +274 -0
  151. atlas/routes/health_routes.py +40 -0
  152. atlas/routes/mcp_auth_routes.py +223 -0
  153. atlas/server_cli.py +164 -0
  154. atlas/tests/conftest.py +20 -0
  155. atlas/tests/integration/test_mcp_auth_integration.py +152 -0
  156. atlas/tests/manual_test_sampling.py +87 -0
  157. atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
  158. atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
  159. atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
  160. atlas/tests/test_agent_roa.py +135 -0
  161. atlas/tests/test_app_factory_smoke.py +47 -0
  162. atlas/tests/test_approval_manager.py +439 -0
  163. atlas/tests/test_atlas_client.py +188 -0
  164. atlas/tests/test_atlas_rag_client.py +447 -0
  165. atlas/tests/test_atlas_rag_integration.py +224 -0
  166. atlas/tests/test_attach_file_flow.py +287 -0
  167. atlas/tests/test_auth_utils.py +165 -0
  168. atlas/tests/test_backend_public_url.py +185 -0
  169. atlas/tests/test_banner_logging.py +287 -0
  170. atlas/tests/test_capability_tokens_and_injection.py +203 -0
  171. atlas/tests/test_compliance_level.py +54 -0
  172. atlas/tests/test_compliance_manager.py +253 -0
  173. atlas/tests/test_config_manager.py +617 -0
  174. atlas/tests/test_config_manager_paths.py +12 -0
  175. atlas/tests/test_core_auth.py +18 -0
  176. atlas/tests/test_core_utils.py +190 -0
  177. atlas/tests/test_docker_env_sync.py +202 -0
  178. atlas/tests/test_domain_errors.py +329 -0
  179. atlas/tests/test_domain_whitelist.py +359 -0
  180. atlas/tests/test_elicitation_manager.py +408 -0
  181. atlas/tests/test_elicitation_routing.py +296 -0
  182. atlas/tests/test_env_demo_server.py +88 -0
  183. atlas/tests/test_error_classification.py +113 -0
  184. atlas/tests/test_error_flow_integration.py +116 -0
  185. atlas/tests/test_feedback_routes.py +333 -0
  186. atlas/tests/test_file_content_extraction.py +1134 -0
  187. atlas/tests/test_file_extraction_routes.py +158 -0
  188. atlas/tests/test_file_library.py +107 -0
  189. atlas/tests/test_file_manager_unit.py +18 -0
  190. atlas/tests/test_health_route.py +49 -0
  191. atlas/tests/test_http_client_stub.py +8 -0
  192. atlas/tests/test_imports_smoke.py +30 -0
  193. atlas/tests/test_interfaces_llm_response.py +9 -0
  194. atlas/tests/test_issue_access_denied_fix.py +136 -0
  195. atlas/tests/test_llm_env_expansion.py +836 -0
  196. atlas/tests/test_log_level_sensitive_data.py +285 -0
  197. atlas/tests/test_mcp_auth_routes.py +341 -0
  198. atlas/tests/test_mcp_client_auth.py +331 -0
  199. atlas/tests/test_mcp_data_injection.py +270 -0
  200. atlas/tests/test_mcp_get_authorized_servers.py +95 -0
  201. atlas/tests/test_mcp_hot_reload.py +512 -0
  202. atlas/tests/test_mcp_image_content.py +424 -0
  203. atlas/tests/test_mcp_logging.py +172 -0
  204. atlas/tests/test_mcp_progress_updates.py +313 -0
  205. atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
  206. atlas/tests/test_mcp_prompts_server.py +39 -0
  207. atlas/tests/test_mcp_tool_result_parsing.py +296 -0
  208. atlas/tests/test_metrics_logger.py +56 -0
  209. atlas/tests/test_middleware_auth.py +379 -0
  210. atlas/tests/test_prompt_risk_and_acl.py +141 -0
  211. atlas/tests/test_rag_mcp_aggregator.py +204 -0
  212. atlas/tests/test_rag_mcp_service.py +224 -0
  213. atlas/tests/test_rate_limit_middleware.py +45 -0
  214. atlas/tests/test_routes_config_smoke.py +60 -0
  215. atlas/tests/test_routes_files_download_token.py +41 -0
  216. atlas/tests/test_routes_files_health.py +18 -0
  217. atlas/tests/test_runtime_imports.py +53 -0
  218. atlas/tests/test_sampling_integration.py +482 -0
  219. atlas/tests/test_security_admin_routes.py +61 -0
  220. atlas/tests/test_security_capability_tokens.py +65 -0
  221. atlas/tests/test_security_file_stats_scope.py +21 -0
  222. atlas/tests/test_security_header_injection.py +191 -0
  223. atlas/tests/test_security_headers_and_filename.py +63 -0
  224. atlas/tests/test_shared_session_repository.py +101 -0
  225. atlas/tests/test_system_prompt_loading.py +181 -0
  226. atlas/tests/test_token_storage.py +505 -0
  227. atlas/tests/test_tool_approval_config.py +93 -0
  228. atlas/tests/test_tool_approval_utils.py +356 -0
  229. atlas/tests/test_tool_authorization_group_filtering.py +223 -0
  230. atlas/tests/test_tool_details_in_config.py +108 -0
  231. atlas/tests/test_tool_planner.py +300 -0
  232. atlas/tests/test_unified_rag_service.py +398 -0
  233. atlas/tests/test_username_override_in_approval.py +258 -0
  234. atlas/tests/test_websocket_auth_header.py +168 -0
  235. atlas/version.py +6 -0
  236. atlas_chat-0.1.0.data/data/.env.example +253 -0
  237. atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
  238. atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
  239. atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
  240. atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
  241. atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
  242. atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
  243. atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
  244. atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
  245. atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
  246. atlas_chat-0.1.0.dist-info/METADATA +236 -0
  247. atlas_chat-0.1.0.dist-info/RECORD +250 -0
  248. atlas_chat-0.1.0.dist-info/WHEEL +5 -0
  249. atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
  250. atlas_chat-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,24 @@
1
+ """RAG module for the chat backend.
2
+
3
+ This module provides:
4
+ - RAG query processing and context retrieval
5
+ - Document metadata and search capabilities
6
+ - Integration with ATLAS RAG API
7
+ """
8
+
9
+ from .atlas_rag_client import AtlasRAGClient, create_atlas_rag_client_from_config
10
+ from .client import DataSource, DocumentMetadata, RAGClient, RAGMetadata, RAGResponse
11
+
12
+ # Create default instance
13
+ rag_client = RAGClient()
14
+
15
+ __all__ = [
16
+ "RAGClient",
17
+ "AtlasRAGClient",
18
+ "create_atlas_rag_client_from_config",
19
+ "DataSource",
20
+ "DocumentMetadata",
21
+ "RAGMetadata",
22
+ "RAGResponse",
23
+ "rag_client",
24
+ ]
@@ -0,0 +1,336 @@
1
+ """ATLAS RAG Client for integrating with the ATLAS RAG API.
2
+
3
+ This client implements the same interface as RAGClient but translates
4
+ requests to the ATLAS RAG API format.
5
+
6
+ ATLAS RAG API:
7
+ - Discovery: GET /discover/datasources?as_user={user}
8
+ - Query: POST /rag/completions?as_user={user}
9
+ """
10
+
11
+ import logging
12
+ from typing import Dict, List, Optional
13
+
14
+ import httpx
15
+ from fastapi import HTTPException
16
+
17
+ from atlas.modules.rag.client import DataSource, DocumentMetadata, RAGMetadata, RAGResponse
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class AtlasRAGClient:
23
+ """Client for communicating with external ATLAS RAG API.
24
+
25
+ Implements the same interface as RAGClient for seamless substitution.
26
+ Uses Bearer token authentication with user impersonation via as_user param.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ base_url: str,
32
+ bearer_token: Optional[str] = None,
33
+ default_model: str = "openai/gpt-oss-120b",
34
+ top_k: int = 4,
35
+ timeout: float = 60.0,
36
+ ):
37
+ """Initialize the external RAG client.
38
+
39
+ Args:
40
+ base_url: Base URL for the external RAG API.
41
+ bearer_token: Bearer token for API authentication.
42
+ default_model: Default model to use for RAG queries.
43
+ top_k: Default number of documents to retrieve.
44
+ timeout: Request timeout in seconds.
45
+ """
46
+ self.base_url = base_url.rstrip("/")
47
+ self.bearer_token = bearer_token
48
+ self.default_model = default_model
49
+ self.top_k = top_k
50
+ self.timeout = timeout
51
+
52
+ logger.info(
53
+ "AtlasRAGClient initialized: url=%s, model=%s, top_k=%d",
54
+ self.base_url,
55
+ self.default_model,
56
+ self.top_k,
57
+ )
58
+
59
+ def _get_headers(self) -> Dict[str, str]:
60
+ """Build HTTP headers for API requests."""
61
+ headers = {"Content-Type": "application/json"}
62
+ if self.bearer_token:
63
+ headers["Authorization"] = f"Bearer {self.bearer_token}"
64
+ return headers
65
+
66
+ async def discover_data_sources(self, user_name: str) -> List[DataSource]:
67
+ """Discover data sources accessible by a user.
68
+
69
+ Calls GET /discover/datasources?as_user={user_name}
70
+
71
+ Args:
72
+ user_name: The username to discover data sources for.
73
+
74
+ Returns:
75
+ List of DataSource objects the user can access.
76
+ """
77
+ logger.info("Discovering data sources for user: %s", user_name)
78
+
79
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
80
+ try:
81
+ response = await client.get(
82
+ f"{self.base_url}/discover/datasources",
83
+ headers=self._get_headers(),
84
+ params={"as_user": user_name},
85
+ )
86
+ response.raise_for_status()
87
+ data = response.json()
88
+
89
+ # Response format: {user_name: str, accessible_data_sources: [{name, compliance_level}]}
90
+ accessible_sources = data.get("accessible_data_sources", [])
91
+ data_sources = [DataSource(**src) for src in accessible_sources]
92
+
93
+ logger.info(
94
+ "Discovered %d data sources for user %s",
95
+ len(data_sources),
96
+ user_name,
97
+ )
98
+ return data_sources
99
+
100
+ except httpx.HTTPStatusError as exc:
101
+ logger.error(
102
+ "HTTP error discovering data sources for %s: %s (status %d)",
103
+ user_name,
104
+ exc.response.text,
105
+ exc.response.status_code,
106
+ )
107
+ return []
108
+
109
+ except httpx.RequestError as exc:
110
+ logger.error(
111
+ "Request error discovering data sources for %s: %s",
112
+ user_name,
113
+ str(exc),
114
+ )
115
+ return []
116
+
117
+ except Exception as exc:
118
+ logger.error(
119
+ "Unexpected error discovering data sources for %s: %s",
120
+ user_name,
121
+ str(exc),
122
+ exc_info=True,
123
+ )
124
+ return []
125
+
126
+ async def query_rag(
127
+ self, user_name: str, data_source: str, messages: List[Dict]
128
+ ) -> RAGResponse:
129
+ """Query RAG endpoint for a response with metadata.
130
+
131
+ Calls POST /rag/completions?as_user={user_name}
132
+
133
+ Args:
134
+ user_name: The username making the query.
135
+ data_source: The data source (corpus) to query.
136
+ messages: List of message dictionaries with role and content.
137
+
138
+ Returns:
139
+ RAGResponse containing content and optional metadata.
140
+
141
+ Raises:
142
+ HTTPException: On API errors (403, 404, 500).
143
+ """
144
+ logger.info(
145
+ "[HTTP-RAG] query_rag called: user=%s, data_source=%s, message_count=%d",
146
+ user_name,
147
+ data_source,
148
+ len(messages),
149
+ )
150
+
151
+ # Extract user query for logging
152
+ user_query = ""
153
+ for msg in reversed(messages):
154
+ if msg.get("role") == "user":
155
+ user_query = msg.get("content", "")[:100]
156
+ break
157
+ logger.debug(
158
+ "[HTTP-RAG] Query preview: %s...",
159
+ user_query,
160
+ )
161
+
162
+ # Build request payload matching RagRequest format
163
+ payload = {
164
+ "messages": messages,
165
+ "stream": False,
166
+ "model": self.default_model,
167
+ "top_k": self.top_k,
168
+ "corpora": [data_source] if data_source else None,
169
+ "threshold": None,
170
+ "expanded_window": [0, 0],
171
+ }
172
+
173
+ logger.debug(
174
+ "[HTTP-RAG] Request payload: model=%s, top_k=%d, corpora=%s",
175
+ payload["model"],
176
+ payload["top_k"],
177
+ payload["corpora"],
178
+ )
179
+
180
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
181
+ try:
182
+ response = await client.post(
183
+ f"{self.base_url}/rag/completions",
184
+ headers=self._get_headers(),
185
+ params={"as_user": user_name},
186
+ json=payload,
187
+ )
188
+ response.raise_for_status()
189
+ data = response.json()
190
+
191
+ logger.debug(
192
+ "[HTTP-RAG] Response received: status=%d, keys=%s",
193
+ response.status_code,
194
+ list(data.keys()),
195
+ )
196
+
197
+ # Check if this is a chat completion (already LLM-interpreted)
198
+ is_completion = data.get("object") == "chat.completion"
199
+
200
+ # Extract content from OpenAI ChatCompletion format
201
+ content = "No response from RAG system."
202
+ if "choices" in data and len(data["choices"]) > 0:
203
+ choice = data["choices"][0]
204
+ if "message" in choice and "content" in choice["message"]:
205
+ content = choice["message"]["content"]
206
+
207
+ logger.debug(
208
+ "[HTTP-RAG] Extracted content: length=%d, is_completion=%s, preview=%s...",
209
+ len(content),
210
+ is_completion,
211
+ content[:300] if content else "(empty)",
212
+ )
213
+
214
+ # Map rag_metadata to RAGMetadata
215
+ metadata = self._parse_rag_metadata(data, data_source)
216
+
217
+ logger.info(
218
+ "[HTTP-RAG] query_rag complete: user=%s, source=%s, content_length=%d, has_metadata=%s, is_completion=%s",
219
+ user_name,
220
+ data_source,
221
+ len(content),
222
+ metadata is not None,
223
+ is_completion,
224
+ )
225
+ return RAGResponse(content=content, metadata=metadata, is_completion=is_completion)
226
+
227
+ except httpx.HTTPStatusError as exc:
228
+ status_code = exc.response.status_code
229
+ logger.error(
230
+ "HTTP error querying RAG for %s: %s (status %d)",
231
+ user_name,
232
+ exc.response.text,
233
+ status_code,
234
+ )
235
+
236
+ if status_code == 403:
237
+ raise HTTPException(
238
+ status_code=403, detail="Access denied to data source"
239
+ )
240
+ elif status_code == 404:
241
+ raise HTTPException(
242
+ status_code=404, detail="Data source not found"
243
+ )
244
+ else:
245
+ raise HTTPException(
246
+ status_code=500, detail="RAG service error"
247
+ )
248
+
249
+ except httpx.RequestError as exc:
250
+ logger.error(
251
+ "Request error querying RAG for %s: %s",
252
+ user_name,
253
+ str(exc),
254
+ )
255
+ raise HTTPException(
256
+ status_code=500, detail="Failed to connect to RAG service"
257
+ )
258
+
259
+ except HTTPException:
260
+ # Re-raise HTTPExceptions
261
+ raise
262
+
263
+ except Exception as exc:
264
+ logger.error(
265
+ "Unexpected error querying RAG for %s: %s",
266
+ user_name,
267
+ str(exc),
268
+ exc_info=True,
269
+ )
270
+ raise HTTPException(status_code=500, detail="Internal server error")
271
+
272
+ def _parse_rag_metadata(
273
+ self, data: Dict, data_source: str
274
+ ) -> Optional[RAGMetadata]:
275
+ """Parse rag_metadata from API response into RAGMetadata model.
276
+
277
+ Args:
278
+ data: The full API response dictionary.
279
+ data_source: The data source used in the query.
280
+
281
+ Returns:
282
+ RAGMetadata if present in response, None otherwise.
283
+ """
284
+ if "rag_metadata" not in data or not data["rag_metadata"]:
285
+ return None
286
+
287
+ try:
288
+ rm = data["rag_metadata"]
289
+
290
+ # Map documents_found to DocumentMetadata list
291
+ documents_found = []
292
+ for doc in rm.get("documents_found", []):
293
+ doc_metadata = DocumentMetadata(
294
+ source=doc.get("corpus_id", ""),
295
+ content_type=doc.get("content_type", "atlas-search"),
296
+ confidence_score=doc.get("confidence_score", 0.0),
297
+ chunk_id=str(doc.get("id")) if doc.get("id") else None,
298
+ last_modified=doc.get("last_modified"),
299
+ )
300
+ documents_found.append(doc_metadata)
301
+
302
+ # Determine data source name from response or fallback
303
+ data_sources_list = rm.get("data_sources", [])
304
+ data_source_name = (
305
+ data_sources_list[0] if data_sources_list else data_source
306
+ )
307
+
308
+ return RAGMetadata(
309
+ query_processing_time_ms=rm.get("query_processing_time_ms", 0),
310
+ total_documents_searched=len(documents_found),
311
+ documents_found=documents_found,
312
+ data_source_name=data_source_name,
313
+ retrieval_method=rm.get("retrieval_method", "similarity"),
314
+ )
315
+
316
+ except Exception as e:
317
+ logger.warning("Failed to parse RAG metadata: %s", str(e))
318
+ return None
319
+
320
+
321
+ def create_atlas_rag_client_from_config(config_manager) -> AtlasRAGClient:
322
+ """Factory function to create AtlasRAGClient from ConfigManager.
323
+
324
+ Args:
325
+ config_manager: ConfigManager instance with app_settings.
326
+
327
+ Returns:
328
+ Configured AtlasRAGClient instance.
329
+ """
330
+ settings = config_manager.app_settings
331
+ return AtlasRAGClient(
332
+ base_url=settings.external_rag_url,
333
+ bearer_token=settings.external_rag_bearer_token,
334
+ default_model=settings.external_rag_default_model,
335
+ top_k=settings.external_rag_top_k,
336
+ )
@@ -0,0 +1,129 @@
1
+ """RAG Client for integrating with RAG mock endpoint."""
2
+
3
+ import logging
4
+ from typing import Dict, List, Optional
5
+
6
+ from fastapi import HTTPException
7
+ from pydantic import BaseModel
8
+
9
+ from atlas.core.http_client import create_rag_client
10
+
11
+
12
+ class DataSource(BaseModel):
13
+ """Represents a RAG data source with compliance information."""
14
+ name: str
15
+ compliance_level: str
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class DocumentMetadata(BaseModel):
21
+ """Metadata about a source document."""
22
+ source: str
23
+ content_type: str
24
+ confidence_score: float
25
+ chunk_id: Optional[str] = None
26
+ last_modified: Optional[str] = None
27
+
28
+
29
+ class RAGMetadata(BaseModel):
30
+ """Metadata about RAG query processing."""
31
+ query_processing_time_ms: int
32
+ total_documents_searched: int
33
+ documents_found: List[DocumentMetadata]
34
+ data_source_name: str
35
+ retrieval_method: str
36
+ query_embedding_time_ms: Optional[int] = None
37
+
38
+
39
+ class RAGResponse(BaseModel):
40
+ """Combined response from RAG system including content and metadata."""
41
+ content: str
42
+ metadata: Optional[RAGMetadata] = None
43
+ is_completion: bool = False # True if content is already LLM-interpreted (from /rag/completions)
44
+
45
+
46
+ class RAGClient:
47
+ """Legacy RAG client for the old rag-mock service.
48
+
49
+ Note: This client is deprecated. Use UnifiedRAGService for RAG operations,
50
+ which handles all RAG sources configured in rag-sources.json.
51
+ """
52
+
53
+ def __init__(self, base_url: str = "http://localhost:8001", timeout: float = 30.0):
54
+ """Initialize the legacy RAG client.
55
+
56
+ Args:
57
+ base_url: Base URL for the RAG mock service.
58
+ timeout: Request timeout in seconds.
59
+ """
60
+ self.base_url = base_url
61
+ self.timeout = timeout
62
+ self.test_client = None
63
+ self.http_client = create_rag_client(self.base_url, self.timeout)
64
+ logger.warning(
65
+ "RAGClient is deprecated. Use UnifiedRAGService for RAG operations. "
66
+ "Configure RAG sources in rag-sources.json."
67
+ )
68
+ logger.info("RAGClient initialized with URL: %s", self.base_url)
69
+
70
+ async def discover_data_sources(self, user_name: str) -> List[DataSource]:
71
+ """Discover data sources accessible by a user.
72
+
73
+ Note: This method is deprecated. Use UnifiedRAGService.discover_data_sources() instead.
74
+ """
75
+ logger.info("discover_data_sources: user=%s (deprecated RAGClient)", user_name)
76
+
77
+ try:
78
+ data = await self.http_client.get(f"/v1/discover/datasources/{user_name}")
79
+ accessible_sources_data = data.get("accessible_data_sources", [])
80
+ except HTTPException as exc:
81
+ logger.warning("HTTP error discovering data sources for %s: %s", user_name, exc.detail)
82
+ return []
83
+ except Exception as exc:
84
+ logger.error("Unexpected error while discovering data sources for %s: %s", user_name, exc, exc_info=True)
85
+ return []
86
+
87
+ return [DataSource(**source_data) for source_data in accessible_sources_data]
88
+
89
+ async def query_rag(self, user_name: str, data_source: str, messages: List[Dict]) -> RAGResponse:
90
+ """Query RAG endpoint for a response with metadata.
91
+
92
+ Note: This method is deprecated. Use UnifiedRAGService.query_rag() instead.
93
+ """
94
+ payload = {
95
+ "messages": messages,
96
+ "user_name": user_name,
97
+ "data_source": data_source,
98
+ "model": "gpt-4-rag-mock",
99
+ "stream": False
100
+ }
101
+
102
+ logger.info("query_rag: user=%s, source=%s (deprecated RAGClient)", user_name, data_source)
103
+
104
+ try:
105
+ data = await self.http_client.post("/v1/chat/completions", json_data=payload)
106
+
107
+ # Extract the assistant message from the response
108
+ content = "No response from RAG system."
109
+ if "choices" in data and len(data["choices"]) > 0:
110
+ choice = data["choices"][0]
111
+ if "message" in choice and "content" in choice["message"]:
112
+ content = choice["message"]["content"]
113
+
114
+ # Extract metadata if present
115
+ metadata = None
116
+ if "rag_metadata" in data and data["rag_metadata"]:
117
+ try:
118
+ metadata = RAGMetadata(**data["rag_metadata"])
119
+ except Exception as e:
120
+ logger.warning(f"Failed to parse RAG metadata: {e}")
121
+
122
+ return RAGResponse(content=content, metadata=metadata)
123
+
124
+ except HTTPException:
125
+ # Re-raise HTTPExceptions from the unified client (they already have proper error handling)
126
+ raise
127
+ except Exception as exc:
128
+ logger.error("Unexpected error while querying RAG for %s: %s", user_name, exc, exc_info=True)
129
+ raise HTTPException(status_code=500, detail="Internal server error")