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,287 @@
1
+ # """
2
+ # LLM calling interface that handles all modes of LLM interaction.
3
+
4
+ # This module provides a clean interface for calling LLMs in different modes:
5
+ # - Plain LLM calls (no tools)
6
+ # - LLM calls with RAG integration
7
+ # - LLM calls with tool support
8
+ # - LLM calls with both RAG and tools
9
+ # """
10
+
11
+ # import asyncio
12
+ # import json
13
+ # import logging
14
+ # import os
15
+ # from typing import Any, Dict, List, Optional
16
+ # from dataclasses import dataclass
17
+
18
+ # import requests
19
+ # from .models import LLMResponse
20
+
21
+ # logger = logging.getLogger(__name__)
22
+
23
+
24
+ # class LLMCaller:
25
+ # """Clean interface for all LLM calling patterns."""
26
+
27
+ # def __init__(self, llm_config=None):
28
+ # """Initialize with optional config dependency injection."""
29
+ # if llm_config is None:
30
+ # from atlas.modules.config import config_manager
31
+ # self.llm_config = config_manager.llm_config
32
+ # else:
33
+ # self.llm_config = llm_config
34
+
35
+ # async def call_plain(self, model_name: str, messages: List[Dict[str, str]]) -> str:
36
+ # """Plain LLM call - no tools, no RAG."""
37
+ # if model_name not in self.llm_config.models:
38
+ # raise ValueError(f"Model {model_name} not found in configuration")
39
+
40
+ # model_config = self.llm_config.models[model_name]
41
+ # api_url = model_config.model_url
42
+ # api_key = os.path.expandvars(model_config.api_key)
43
+ # model_id = model_config.model_name
44
+
45
+ # headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
46
+ # # Inject any extra provider-specific headers (expand env vars, skip unresolved placeholders)
47
+ # if getattr(model_config, 'extra_headers', None):
48
+ # for h_key, h_val in model_config.extra_headers.items():
49
+ # if not h_val:
50
+ # continue
51
+ # expanded = os.path.expandvars(h_val)
52
+ # # Skip if still looks like an unresolved ${VAR}
53
+ # if expanded.startswith("${") and expanded.endswith("}"):
54
+ # continue
55
+ # headers[h_key] = expanded
56
+ # payload = {"model": model_id, "messages": messages, "max_tokens": model_config.max_tokens, "temperature": 0.7}
57
+
58
+ # VERBOSE = True
59
+
60
+ # try:
61
+ # total_chars = sum(len(str(msg.get('content', ''))) for msg in messages)
62
+ # logger.info(f"Plain LLM call: {len(messages)} messages, {total_chars} chars")
63
+
64
+ # # if verbose, just print the "message" list
65
+ # if VERBOSE:
66
+ # logging.info(f"Messages are: {[m for m in messages]}")
67
+
68
+ # loop = asyncio.get_event_loop()
69
+ # response = await loop.run_in_executor(
70
+ # None, lambda: requests.post(api_url, headers=headers, json=payload, timeout=30)
71
+ # )
72
+
73
+ # if response.status_code == 200:
74
+ # result = response.json()
75
+ # llm_response = result["choices"][0]["message"]["content"]
76
+ # logger.info(f"LLM response preview: '{llm_response[:200]}{'...' if len(llm_response) > 200 else ''}'")
77
+ # return llm_response
78
+
79
+ # logger.error("LLM API error %s: %s", response.status_code, response.text)
80
+ # raise Exception(f"LLM API error: {response.status_code}")
81
+
82
+ # except requests.RequestException as exc:
83
+ # logger.error("Request error calling LLM: %s", exc, exc_info=True)
84
+ # raise Exception(f"Failed to call LLM: {exc}")
85
+ # except KeyError as exc:
86
+ # logger.error("Invalid response format from LLM: %s", exc, exc_info=True)
87
+ # raise Exception("Invalid response format from LLM")
88
+
89
+ # async def call_with_rag(
90
+ # self,
91
+ # model_name: str,
92
+ # messages: List[Dict[str, str]],
93
+ # data_sources: List[str],
94
+ # user_email: str,
95
+ # rag_client=None
96
+ # ) -> str:
97
+ # """LLM call with RAG integration."""
98
+ # if not data_sources:
99
+ # return await self.call_plain(model_name, messages)
100
+
101
+ # # Import RAG client if not provided
102
+ # if rag_client is None:
103
+ # from atlas.modules.rag import rag_client as default_rag_client
104
+ # rag_client = default_rag_client
105
+
106
+ # # Use the first selected data source
107
+ # data_source = data_sources[0]
108
+
109
+ # try:
110
+ # # Query RAG for context
111
+ # rag_response = await rag_client.query_rag(
112
+ # user_email,
113
+ # data_source,
114
+ # messages
115
+ # )
116
+
117
+ # # Integrate RAG context into messages
118
+ # messages_with_rag = messages.copy()
119
+ # rag_context_message = {
120
+ # "role": "system",
121
+ # "content": f"Retrieved context from {data_source}:\n\n{rag_response.content}\n\nUse this context to inform your response."
122
+ # }
123
+ # messages_with_rag.insert(-1, rag_context_message)
124
+
125
+ # # Call LLM with enriched context
126
+ # llm_response = await self.call_plain(model_name, messages_with_rag)
127
+
128
+ # # Append metadata if available
129
+ # if rag_response.metadata:
130
+ # metadata_summary = self._format_rag_metadata(rag_response.metadata)
131
+ # llm_response += f"\n\n---\n**RAG Sources & Processing Info:**\n{metadata_summary}"
132
+
133
+ # return llm_response
134
+
135
+ # except Exception as exc:
136
+ # logger.error(f"Error in RAG-integrated query: {exc}")
137
+ # # Fallback to plain LLM call
138
+ # return await self.call_plain(model_name, messages)
139
+
140
+ # async def call_with_tools(
141
+ # self,
142
+ # model_name: str,
143
+ # messages: List[Dict[str, str]],
144
+ # tools_schema: List[Dict],
145
+ # tool_choice: str = "auto"
146
+ # ) -> LLMResponse:
147
+ # """LLM call with tool support."""
148
+ # if not tools_schema:
149
+ # content = await self.call_plain(model_name, messages)
150
+ # return LLMResponse(content=content, model_used=model_name)
151
+
152
+ # if model_name not in self.llm_config.models:
153
+ # raise ValueError(f"Model {model_name} not found in configuration")
154
+
155
+ # model_config = self.llm_config.models[model_name]
156
+ # api_url = model_config.model_url
157
+ # api_key = os.path.expandvars(model_config.api_key)
158
+ # model_id = model_config.model_name
159
+
160
+ # headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
161
+ # if getattr(model_config, 'extra_headers', None):
162
+ # for h_key, h_val in model_config.extra_headers.items():
163
+ # if not h_val:
164
+ # continue
165
+ # expanded = os.path.expandvars(h_val)
166
+ # if expanded.startswith("${") and expanded.endswith("}"):
167
+ # continue
168
+ # headers[h_key] = expanded
169
+ # payload = {
170
+ # "model": model_id,
171
+ # "messages": messages,
172
+ # "tools": tools_schema,
173
+ # "tool_choice": tool_choice,
174
+ # "max_tokens": model_config.max_tokens,
175
+ # "temperature": 0.7,
176
+ # }
177
+
178
+ # try:
179
+ # total_chars = sum(len(str(msg.get('content', ''))) for msg in messages)
180
+ # logger.info(f"LLM call with tools: {len(messages)} messages, {total_chars} chars, {len(tools_schema)} tools")
181
+
182
+ # loop = asyncio.get_event_loop()
183
+ # response = await loop.run_in_executor(
184
+ # None, lambda: requests.post(api_url, headers=headers, json=payload, timeout=30)
185
+ # )
186
+
187
+ # if response.status_code != 200:
188
+ # logger.error("LLM API error %s: %s", response.status_code, response.text)
189
+ # raise Exception(f"LLM API error: {response.status_code}")
190
+
191
+ # result = response.json()
192
+ # choice = result["choices"][0]
193
+ # message = choice["message"]
194
+
195
+ # return LLMResponse(
196
+ # content=message.get("content", ""),
197
+ # tool_calls=message.get("tool_calls"),
198
+ # model_used=model_name
199
+ # )
200
+
201
+ # except requests.RequestException as exc:
202
+ # logger.error("Request error calling LLM with tools: %s", exc, exc_info=True)
203
+ # raise Exception(f"Failed to call LLM: {exc}")
204
+ # except KeyError as exc:
205
+ # logger.error("Invalid response format from LLM: %s", exc, exc_info=True)
206
+ # raise Exception("Invalid response format from LLM")
207
+
208
+ # async def call_with_rag_and_tools(
209
+ # self,
210
+ # model_name: str,
211
+ # messages: List[Dict[str, str]],
212
+ # data_sources: List[str],
213
+ # tools_schema: List[Dict],
214
+ # user_email: str,
215
+ # tool_choice: str = "auto",
216
+ # rag_client=None
217
+ # ) -> LLMResponse:
218
+ # """Full integration: RAG + Tools."""
219
+ # if not data_sources:
220
+ # return await self.call_with_tools(model_name, messages, tools_schema, tool_choice)
221
+
222
+ # # Import RAG client if not provided
223
+ # if rag_client is None:
224
+ # from atlas.modules.rag import rag_client as default_rag_client
225
+ # rag_client = default_rag_client
226
+
227
+ # # Use the first selected data source
228
+ # data_source = data_sources[0]
229
+
230
+ # try:
231
+ # # Query RAG for context
232
+ # rag_response = await rag_client.query_rag(
233
+ # user_email,
234
+ # data_source,
235
+ # messages
236
+ # )
237
+
238
+ # # Integrate RAG context into messages
239
+ # messages_with_rag = messages.copy()
240
+ # rag_context_message = {
241
+ # "role": "system",
242
+ # "content": f"Retrieved context from {data_source}:\n\n{rag_response.content}\n\nUse this context to inform your response."
243
+ # }
244
+ # messages_with_rag.insert(-1, rag_context_message)
245
+
246
+ # # Call LLM with enriched context and tools
247
+ # llm_response = await self.call_with_tools(model_name, messages_with_rag, tools_schema, tool_choice)
248
+
249
+ # # Append metadata to content if available and no tool calls
250
+ # if rag_response.metadata and not llm_response.has_tool_calls():
251
+ # metadata_summary = self._format_rag_metadata(rag_response.metadata)
252
+ # llm_response.content += f"\n\n---\n**RAG Sources & Processing Info:**\n{metadata_summary}"
253
+
254
+ # return llm_response
255
+
256
+ # except Exception as exc:
257
+ # logger.error(f"Error in RAG+tools integrated query: {exc}")
258
+ # # Fallback to tools-only call
259
+ # return await self.call_with_tools(model_name, messages, tools_schema, tool_choice)
260
+
261
+ # def _format_rag_metadata(self, metadata) -> str:
262
+ # """Format RAG metadata into a user-friendly summary."""
263
+ # # Import here to avoid circular imports
264
+ # try:
265
+ # from atlas.modules.rag.models import RAGMetadata
266
+ # if not isinstance(metadata, RAGMetadata):
267
+ # return "Metadata unavailable"
268
+ # except ImportError:
269
+ # return "Metadata unavailable"
270
+
271
+ # summary_parts = []
272
+ # summary_parts.append(f" **Data Source:** {metadata.data_source_name}")
273
+ # summary_parts.append(f" **Processing Time:** {metadata.query_processing_time_ms}ms")
274
+
275
+ # if metadata.documents_found:
276
+ # summary_parts.append(f" **Documents Found:** {len(metadata.documents_found)} (searched {metadata.total_documents_searched})")
277
+
278
+ # for i, doc in enumerate(metadata.documents_found[:3]):
279
+ # confidence_percent = int(doc.confidence_score * 100)
280
+ # summary_parts.append(f" • {doc.source} ({confidence_percent}% relevance, {doc.content_type})")
281
+
282
+ # if len(metadata.documents_found) > 3:
283
+ # remaining = len(metadata.documents_found) - 3
284
+ # summary_parts.append(f" • ... and {remaining} more document(s)")
285
+
286
+ # summary_parts.append(f" **Retrieval Method:** {metadata.retrieval_method}")
287
+ # return "\n".join(summary_parts)