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,179 @@
1
+ """Tools mode runner - handles LLM calls with tool execution."""
2
+
3
+ import logging
4
+ from typing import Any, Awaitable, Callable, Dict, List, Optional
5
+
6
+ from atlas.domain.messages.models import Message, MessageRole, ToolResult
7
+ from atlas.domain.sessions.models import Session
8
+ from atlas.interfaces.events import EventPublisher
9
+ from atlas.interfaces.llm import LLMProtocol
10
+ from atlas.interfaces.tools import ToolManagerProtocol
11
+ from atlas.modules.prompts.prompt_provider import PromptProvider
12
+
13
+ from ..preprocessors.message_builder import build_session_context
14
+ from ..utilities import error_handler, event_notifier, tool_executor
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # Type hint for the update callback
19
+ UpdateCallback = Callable[[Dict[str, Any]], Awaitable[None]]
20
+
21
+
22
+ class ToolsModeRunner:
23
+ """
24
+ Runner for tools mode.
25
+
26
+ Executes LLM calls with tool integration, including tool execution
27
+ and artifact processing.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ llm: LLMProtocol,
33
+ tool_manager: ToolManagerProtocol,
34
+ event_publisher: EventPublisher,
35
+ prompt_provider: Optional[PromptProvider] = None,
36
+ artifact_processor: Optional[Callable[[Session, List[ToolResult], Optional[UpdateCallback]], Awaitable[None]]] = None,
37
+ config_manager=None,
38
+ ):
39
+ """
40
+ Initialize tools mode runner.
41
+
42
+ Args:
43
+ llm: LLM protocol implementation
44
+ tool_manager: Tool manager for tool execution
45
+ event_publisher: Event publisher for UI updates
46
+ prompt_provider: Optional prompt provider
47
+ artifact_processor: Optional callback for processing tool artifacts
48
+ config_manager: Optional config manager for approval settings
49
+ """
50
+ self.llm = llm
51
+ self.tool_manager = tool_manager
52
+ self.event_publisher = event_publisher
53
+ self.prompt_provider = prompt_provider
54
+ self.artifact_processor = artifact_processor
55
+ self.config_manager = config_manager
56
+ self.skip_approval = False
57
+
58
+ # Verify event_publisher has send_json for elicitation support
59
+ if hasattr(event_publisher, 'send_json'):
60
+ logger.debug(f"ToolsModeRunner initialized with event_publisher that has send_json: {type(event_publisher)}")
61
+ else:
62
+ logger.warning(f"ToolsModeRunner initialized with event_publisher WITHOUT send_json: {type(event_publisher)}")
63
+
64
+ async def run(
65
+ self,
66
+ session: Session,
67
+ model: str,
68
+ messages: List[Dict[str, Any]],
69
+ selected_tools: List[str],
70
+ selected_data_sources: Optional[List[str]] = None,
71
+ user_email: Optional[str] = None,
72
+ tool_choice_required: bool = False,
73
+ update_callback: Optional[UpdateCallback] = None,
74
+ temperature: float = 0.7,
75
+ ) -> Dict[str, Any]:
76
+ """
77
+ Execute tools mode.
78
+
79
+ Args:
80
+ session: Current chat session
81
+ model: LLM model to use
82
+ messages: Message history
83
+ selected_tools: List of tools to make available
84
+ selected_data_sources: Optional list of data sources (for RAG+tools)
85
+ user_email: Optional user email for authorization
86
+ tool_choice_required: Whether tool use is required
87
+ update_callback: Optional callback for streaming updates
88
+ temperature: LLM temperature parameter
89
+
90
+ Returns:
91
+ Response dictionary
92
+ """
93
+ # Resolve tool schemas
94
+ tools_schema = await error_handler.safe_get_tools_schema(self.tool_manager, selected_tools)
95
+
96
+ # Call LLM with tools (and RAG if provided)
97
+ llm_response = await error_handler.safe_call_llm_with_tools(
98
+ llm_caller=self.llm,
99
+ model=model,
100
+ messages=messages,
101
+ tools_schema=tools_schema,
102
+ data_sources=selected_data_sources,
103
+ user_email=user_email,
104
+ tool_choice=("required" if tool_choice_required else "auto"),
105
+ temperature=temperature,
106
+ )
107
+
108
+ # No tool calls -> treat as plain content
109
+ if not llm_response or not llm_response.has_tool_calls():
110
+ content = llm_response.content if llm_response else ""
111
+ assistant_message = Message(role=MessageRole.ASSISTANT, content=content)
112
+ session.history.add_message(assistant_message)
113
+
114
+ await self.event_publisher.publish_chat_response(
115
+ message=content,
116
+ has_pending_tools=False,
117
+ )
118
+ await self.event_publisher.publish_response_complete()
119
+
120
+ return event_notifier.create_chat_response(content)
121
+
122
+ # Execute tool workflow
123
+ session_context = build_session_context(session)
124
+
125
+ # Ensure update_callback is never None (critical for elicitation)
126
+ effective_callback = update_callback
127
+ if effective_callback is None:
128
+ effective_callback = self._get_send_json()
129
+ logger.debug("Tools mode: update_callback was None, using event_publisher.send_json fallback")
130
+
131
+ if effective_callback is None:
132
+ logger.warning("Tools mode: No update callback available - elicitation will not work!")
133
+
134
+ final_response, tool_results = await tool_executor.execute_tools_workflow(
135
+ llm_response=llm_response,
136
+ messages=messages,
137
+ model=model,
138
+ session_context=session_context,
139
+ tool_manager=self.tool_manager,
140
+ llm_caller=self.llm,
141
+ prompt_provider=self.prompt_provider,
142
+ update_callback=effective_callback,
143
+ config_manager=self.config_manager,
144
+ skip_approval=self.skip_approval,
145
+ user_email=user_email,
146
+ )
147
+
148
+ # Process artifacts if handler provided
149
+ if self.artifact_processor:
150
+ await self.artifact_processor(session, tool_results, effective_callback)
151
+
152
+ # Add final assistant message to history
153
+ assistant_message = Message(
154
+ role=MessageRole.ASSISTANT,
155
+ content=final_response,
156
+ metadata={
157
+ "tools": selected_tools,
158
+ **({"data_sources": selected_data_sources} if selected_data_sources else {}),
159
+ },
160
+ )
161
+ session.history.add_message(assistant_message)
162
+
163
+ # Emit final chat response
164
+ await self.event_publisher.publish_chat_response(
165
+ message=final_response,
166
+ has_pending_tools=False,
167
+ )
168
+ await self.event_publisher.publish_response_complete()
169
+
170
+ return event_notifier.create_chat_response(final_response)
171
+
172
+ def _get_send_json(self) -> Optional[UpdateCallback]:
173
+ """Get send_json callback from event publisher if available."""
174
+ if hasattr(self.event_publisher, 'send_json'):
175
+ callback = self.event_publisher.send_json
176
+ logger.debug(f"_get_send_json: event_publisher.send_json = {callback is not None}")
177
+ return callback
178
+ logger.warning(f"_get_send_json: event_publisher does not have send_json method. Type: {type(self.event_publisher)}")
179
+ return None
@@ -0,0 +1,213 @@
1
+ """Chat orchestrator - coordinates the full chat request flow."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, List, Optional
5
+ from uuid import UUID
6
+
7
+ from atlas.domain.errors import SessionNotFoundError
8
+ from atlas.domain.messages.models import Message, MessageRole
9
+ from atlas.interfaces.events import EventPublisher
10
+ from atlas.interfaces.llm import LLMProtocol
11
+ from atlas.interfaces.sessions import SessionRepository
12
+ from atlas.interfaces.tools import ToolManagerProtocol
13
+ from atlas.modules.prompts.prompt_provider import PromptProvider
14
+
15
+ from .modes.agent import AgentModeRunner
16
+ from .modes.plain import PlainModeRunner
17
+ from .modes.rag import RagModeRunner
18
+ from .modes.tools import ToolsModeRunner
19
+ from .policies.tool_authorization import ToolAuthorizationService
20
+ from .preprocessors.message_builder import MessageBuilder
21
+ from .preprocessors.prompt_override_service import PromptOverrideService
22
+ from .utilities import file_processor
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class ChatOrchestrator:
28
+ """
29
+ Orchestrates the full chat request flow.
30
+
31
+ Coordinates preprocessing, policy checks, mode selection, and execution.
32
+ Provides clean separation between request handling and business logic.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ llm: LLMProtocol,
38
+ event_publisher: EventPublisher,
39
+ session_repository: SessionRepository,
40
+ tool_manager: Optional[ToolManagerProtocol] = None,
41
+ prompt_provider: Optional[PromptProvider] = None,
42
+ file_manager: Optional[Any] = None,
43
+ artifact_processor: Optional[Any] = None,
44
+ plain_mode: Optional[PlainModeRunner] = None,
45
+ rag_mode: Optional[RagModeRunner] = None,
46
+ tools_mode: Optional[ToolsModeRunner] = None,
47
+ agent_mode: Optional[AgentModeRunner] = None,
48
+ ):
49
+ """
50
+ Initialize chat orchestrator.
51
+
52
+ Args:
53
+ llm: LLM protocol implementation
54
+ event_publisher: Event publisher for UI updates
55
+ session_repository: Session storage repository
56
+ tool_manager: Optional tool manager
57
+ prompt_provider: Optional prompt provider
58
+ file_manager: Optional file manager
59
+ artifact_processor: Optional artifact processor callback
60
+ plain_mode: Optional pre-configured plain mode runner
61
+ rag_mode: Optional pre-configured RAG mode runner
62
+ tools_mode: Optional pre-configured tools mode runner
63
+ agent_mode: Optional pre-configured agent mode runner
64
+ """
65
+ self.llm = llm
66
+ self.event_publisher = event_publisher
67
+ self.session_repository = session_repository
68
+ self.tool_manager = tool_manager
69
+ self.prompt_provider = prompt_provider
70
+ self.file_manager = file_manager
71
+
72
+ # Initialize services
73
+ self.tool_authorization = ToolAuthorizationService(tool_manager=tool_manager)
74
+ self.prompt_override = PromptOverrideService(tool_manager=tool_manager)
75
+ self.message_builder = MessageBuilder(prompt_provider=prompt_provider)
76
+
77
+ # Initialize or use provided mode runners
78
+ self.plain_mode = plain_mode or PlainModeRunner(
79
+ llm=llm,
80
+ event_publisher=event_publisher,
81
+ )
82
+ self.rag_mode = rag_mode or RagModeRunner(
83
+ llm=llm,
84
+ event_publisher=event_publisher,
85
+ )
86
+ self.tools_mode = tools_mode or ToolsModeRunner(
87
+ llm=llm,
88
+ tool_manager=tool_manager,
89
+ event_publisher=event_publisher,
90
+ prompt_provider=prompt_provider,
91
+ artifact_processor=artifact_processor,
92
+ )
93
+ self.agent_mode = agent_mode
94
+
95
+ async def execute(
96
+ self,
97
+ session_id: UUID,
98
+ content: str,
99
+ model: str,
100
+ user_email: Optional[str] = None,
101
+ selected_tools: Optional[List[str]] = None,
102
+ selected_prompts: Optional[List[str]] = None,
103
+ selected_data_sources: Optional[List[str]] = None,
104
+ only_rag: bool = False,
105
+ tool_choice_required: bool = False,
106
+ agent_mode: bool = False,
107
+ temperature: float = 0.7,
108
+ files: Optional[Dict[str, Any]] = None,
109
+ **kwargs
110
+ ) -> Dict[str, Any]:
111
+ """
112
+ Execute a chat request through the full pipeline.
113
+
114
+ Args:
115
+ session_id: Session identifier
116
+ content: User message content
117
+ model: LLM model to use
118
+ user_email: Optional user email
119
+ selected_tools: Optional list of tools
120
+ selected_prompts: Optional list of MCP prompts
121
+ selected_data_sources: Optional list of data sources
122
+ only_rag: Whether to use only RAG (no tools)
123
+ tool_choice_required: Whether tool use is required
124
+ agent_mode: Whether to use agent mode
125
+ temperature: LLM temperature
126
+ files: Optional files to attach
127
+ **kwargs: Additional parameters
128
+
129
+ Returns:
130
+ Response dictionary
131
+ """
132
+ # Get session from repository
133
+ session = await self.session_repository.get(session_id)
134
+ if not session:
135
+ raise SessionNotFoundError(f"Session {session_id} not found")
136
+
137
+ # Add user message to history
138
+ user_message = Message(
139
+ role=MessageRole.USER,
140
+ content=content,
141
+ metadata={"model": model}
142
+ )
143
+ session.history.add_message(user_message)
144
+ session.update_timestamp()
145
+
146
+ # Handle file ingestion
147
+ update_callback = kwargs.get("update_callback")
148
+ logger.debug(f"Orchestrator.execute: update_callback present = {update_callback is not None}")
149
+ session.context = await file_processor.handle_session_files(
150
+ session_context=session.context,
151
+ user_email=user_email,
152
+ files_map=files,
153
+ file_manager=self.file_manager,
154
+ update_callback=update_callback
155
+ )
156
+
157
+ # Build messages with history and files manifest
158
+ messages = await self.message_builder.build_messages(
159
+ session=session,
160
+ include_files_manifest=True
161
+ )
162
+
163
+ # Apply MCP prompt override
164
+ messages = await self.prompt_override.apply_prompt_override(
165
+ messages=messages,
166
+ selected_prompts=selected_prompts
167
+ )
168
+
169
+ # Route to appropriate mode
170
+ if agent_mode and self.agent_mode:
171
+ return await self.agent_mode.run(
172
+ session=session,
173
+ model=model,
174
+ messages=messages,
175
+ selected_tools=selected_tools,
176
+ selected_data_sources=selected_data_sources,
177
+ max_steps=kwargs.get("agent_max_steps", 30),
178
+ temperature=temperature,
179
+ agent_loop_strategy=kwargs.get("agent_loop_strategy"),
180
+ )
181
+ elif selected_tools and not only_rag:
182
+ # Apply tool authorization
183
+ selected_tools = await self.tool_authorization.filter_authorized_tools(
184
+ selected_tools=selected_tools,
185
+ user_email=user_email
186
+ )
187
+ return await self.tools_mode.run(
188
+ session=session,
189
+ model=model,
190
+ messages=messages,
191
+ selected_tools=selected_tools,
192
+ selected_data_sources=selected_data_sources,
193
+ user_email=user_email,
194
+ tool_choice_required=tool_choice_required,
195
+ update_callback=update_callback,
196
+ temperature=temperature,
197
+ )
198
+ elif selected_data_sources:
199
+ return await self.rag_mode.run(
200
+ session=session,
201
+ model=model,
202
+ messages=messages,
203
+ data_sources=selected_data_sources,
204
+ user_email=user_email,
205
+ temperature=temperature,
206
+ )
207
+ else:
208
+ return await self.plain_mode.run(
209
+ session=session,
210
+ model=model,
211
+ messages=messages,
212
+ temperature=temperature,
213
+ )
@@ -0,0 +1 @@
1
+ """Policy modules for chat application."""
@@ -0,0 +1,99 @@
1
+ """Tool authorization policy - filters tools based on user access control."""
2
+
3
+ import logging
4
+ from typing import Any, List, Optional
5
+
6
+ from atlas.core.auth import is_user_in_group
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class ToolAuthorizationService:
12
+ """
13
+ Service that filters selected tools based on user authorization.
14
+
15
+ Enforces MCP tool access control lists (ACLs) by checking:
16
+ - User authorization to MCP servers
17
+ - Special cases (e.g., canvas_canvas tool is always allowed)
18
+ """
19
+
20
+ def __init__(self, tool_manager: Optional[Any] = None):
21
+ """
22
+ Initialize the tool authorization service.
23
+
24
+ Args:
25
+ tool_manager: Optional tool manager with server configuration
26
+ """
27
+ self.tool_manager = tool_manager
28
+
29
+ async def filter_authorized_tools(
30
+ self,
31
+ selected_tools: List[str],
32
+ user_email: Optional[str] = None,
33
+ ) -> List[str]:
34
+ """
35
+ Filter tools to only those the user is authorized to use.
36
+
37
+ Args:
38
+ selected_tools: List of tool names (format: "server_toolname")
39
+ user_email: Email of the user making the request
40
+
41
+ Returns:
42
+ Filtered list of authorized tool names
43
+ """
44
+ if not selected_tools or not self.tool_manager:
45
+ return selected_tools or []
46
+
47
+ try:
48
+ user = user_email or ""
49
+
50
+ # Get authorized servers for this user
51
+ authorized_servers = await self._get_authorized_servers(user)
52
+
53
+ # Filter tools by server prefix
54
+ filtered_tools: List[str] = []
55
+ for tool in selected_tools:
56
+ # Special case: canvas_canvas is always allowed
57
+ if tool == "canvas_canvas":
58
+ filtered_tools.append(tool)
59
+ continue
60
+
61
+ # Check if tool belongs to an authorized server
62
+ if isinstance(tool, str) and "_" in tool:
63
+ # Match against authorized servers by checking if tool name starts with server_
64
+ # This handles server names that contain underscores (e.g., "pptx_generator")
65
+ matched_server = None
66
+ for auth_server in authorized_servers:
67
+ if tool.startswith(f"{auth_server}_"):
68
+ matched_server = auth_server
69
+ break
70
+
71
+ if matched_server:
72
+ filtered_tools.append(tool)
73
+
74
+ return filtered_tools
75
+
76
+ except Exception:
77
+ logger.debug(
78
+ "Tool ACL filtering failed; proceeding with original selection",
79
+ exc_info=True
80
+ )
81
+ return selected_tools
82
+
83
+ async def _get_authorized_servers(self, user: str) -> List[str]:
84
+ """
85
+ Get list of MCP servers the user is authorized to access.
86
+
87
+ Args:
88
+ user: User email
89
+
90
+ Returns:
91
+ List of authorized server names
92
+ """
93
+ # Use tool_manager's authorization method if available
94
+ if hasattr(self.tool_manager, "get_authorized_servers"):
95
+ return await self.tool_manager.get_authorized_servers(user, is_user_in_group) # type: ignore[attr-defined]
96
+
97
+ # If no authorization method available, return empty list (no authorized servers)
98
+ logger.warning("Tool manager has no get_authorized_servers method for user %s", user)
99
+ return []
@@ -0,0 +1 @@
1
+ """Preprocessor modules for chat application."""
@@ -0,0 +1,92 @@
1
+ """Message builder - constructs messages with history and files manifest."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from atlas.domain.sessions.models import Session
7
+ from atlas.modules.prompts.prompt_provider import PromptProvider
8
+
9
+ from ..utilities import file_processor
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def build_session_context(session: Session) -> Dict[str, Any]:
15
+ """
16
+ Build session context dictionary from session.
17
+
18
+ Args:
19
+ session: Chat session
20
+
21
+ Returns:
22
+ Session context dictionary
23
+ """
24
+ return {
25
+ "session_id": session.id,
26
+ "user_email": session.user_email,
27
+ "files": session.context.get("files", {}),
28
+ **session.context
29
+ }
30
+
31
+
32
+ class MessageBuilder:
33
+ """
34
+ Service that builds complete message arrays for LLM calls.
35
+
36
+ Combines conversation history with files manifest and system prompt.
37
+ """
38
+
39
+ def __init__(self, prompt_provider: Optional[PromptProvider] = None):
40
+ """
41
+ Initialize message builder.
42
+
43
+ Args:
44
+ prompt_provider: Optional prompt provider for loading system prompt
45
+ """
46
+ self.prompt_provider = prompt_provider
47
+
48
+ async def build_messages(
49
+ self,
50
+ session: Session,
51
+ include_files_manifest: bool = True,
52
+ include_system_prompt: bool = True,
53
+ ) -> List[Dict[str, Any]]:
54
+ """
55
+ Build messages array from session history and context.
56
+
57
+ Args:
58
+ session: Current chat session
59
+ include_files_manifest: Whether to append files manifest
60
+ include_system_prompt: Whether to prepend system prompt
61
+
62
+ Returns:
63
+ List of messages ready for LLM call
64
+ """
65
+ messages = []
66
+
67
+ # Optionally add system prompt at the beginning
68
+ if include_system_prompt and self.prompt_provider:
69
+ system_prompt = self.prompt_provider.get_system_prompt(
70
+ user_email=session.user_email
71
+ )
72
+ if system_prompt:
73
+ messages.append({"role": "system", "content": system_prompt})
74
+ logger.debug(f"Added system prompt (len={len(system_prompt)})")
75
+
76
+ # Get conversation history from session
77
+ history_messages = session.history.get_messages_for_llm()
78
+ messages.extend(history_messages)
79
+
80
+ # Optionally add files manifest
81
+ if include_files_manifest:
82
+ session_context = build_session_context(session)
83
+ files_in_context = session_context.get("files", {})
84
+ logger.debug(f"Session has {len(files_in_context)} files: {list(files_in_context.keys())}")
85
+ files_manifest = file_processor.build_files_manifest(session_context)
86
+ if files_manifest:
87
+ logger.debug(f"Adding files manifest to messages: {files_manifest['content'][:100]}")
88
+ messages.append(files_manifest)
89
+ else:
90
+ logger.warning("No files manifest generated despite include_files_manifest=True")
91
+
92
+ return messages
@@ -0,0 +1,104 @@
1
+ """Prompt override service - handles MCP system prompt injection."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class PromptOverrideService:
10
+ """
11
+ Service that handles MCP prompt override injection.
12
+
13
+ Retrieves MCP-provided prompts and injects them as system messages,
14
+ applying only the first valid prompt found.
15
+ """
16
+
17
+ def __init__(self, tool_manager: Optional[Any] = None):
18
+ """
19
+ Initialize the prompt override service.
20
+
21
+ Args:
22
+ tool_manager: Optional tool manager with prompt retrieval capability
23
+ """
24
+ self.tool_manager = tool_manager
25
+
26
+ async def apply_prompt_override(
27
+ self,
28
+ messages: List[Dict[str, Any]],
29
+ selected_prompts: Optional[List[str]] = None,
30
+ ) -> List[Dict[str, Any]]:
31
+ """
32
+ Apply MCP prompt override if selected prompts are provided.
33
+
34
+ Only the first valid prompt is applied, prepended as a system message.
35
+
36
+ Args:
37
+ messages: Current message history
38
+ selected_prompts: List of prompt keys (format: "server_promptname")
39
+
40
+ Returns:
41
+ Messages with prompt override prepended (if applicable)
42
+ """
43
+ if not selected_prompts or not self.tool_manager:
44
+ return messages
45
+
46
+ try:
47
+ # Iterate in order; when found, fetch prompt content and inject
48
+ for key in selected_prompts:
49
+ if not isinstance(key, str) or "_" not in key:
50
+ continue
51
+
52
+ server, prompt_name = key.split("_", 1)
53
+
54
+ # Retrieve prompt from MCP
55
+ try:
56
+ prompt_obj = await self.tool_manager.get_prompt(server, prompt_name)
57
+ prompt_text = self._extract_prompt_text(prompt_obj)
58
+
59
+ if prompt_text:
60
+ # Prepend as system message override
61
+ messages = [{"role": "system", "content": prompt_text}] + messages
62
+ logger.info(
63
+ "Applied MCP system prompt override (len=%d)",
64
+ len(prompt_text),
65
+ )
66
+ break # apply only one
67
+
68
+ except Exception:
69
+ logger.debug("Failed retrieving MCP prompt %s", key, exc_info=True)
70
+
71
+ except Exception:
72
+ logger.debug(
73
+ "Prompt override injection skipped due to non-fatal error",
74
+ exc_info=True
75
+ )
76
+
77
+ return messages
78
+
79
+ def _extract_prompt_text(self, prompt_obj: Any) -> Optional[str]:
80
+ """
81
+ Extract text content from various MCP prompt object formats.
82
+
83
+ Args:
84
+ prompt_obj: Prompt object from MCP (could be string or structured object)
85
+
86
+ Returns:
87
+ Extracted prompt text, or None if extraction failed
88
+ """
89
+ # Simple string case
90
+ if isinstance(prompt_obj, str):
91
+ return prompt_obj
92
+
93
+ # FastMCP PromptMessage-like: may have 'content' list with text entries
94
+ if hasattr(prompt_obj, "content"):
95
+ content_field = getattr(prompt_obj, "content")
96
+
97
+ # content could be list of objects with 'text'
98
+ if isinstance(content_field, list) and content_field:
99
+ first = content_field[0]
100
+ if hasattr(first, "text") and isinstance(first.text, str):
101
+ return first.text
102
+
103
+ # Fallback: string dump
104
+ return str(prompt_obj)