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,191 @@
1
+ """
2
+ Elicitation manager for handling user input requests during tool execution.
3
+
4
+ This manager coordinates between MCP servers requesting user input (via ctx.elicit())
5
+ and the frontend UI collecting that input. It provides a synchronization mechanism
6
+ where tool execution pauses until the user responds.
7
+ """
8
+
9
+ import asyncio
10
+ import logging
11
+ from dataclasses import dataclass, field
12
+ from datetime import datetime, timezone
13
+ from typing import Any, Dict, Optional
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class ElicitationRequest:
20
+ """Represents a pending elicitation request awaiting user response."""
21
+ elicitation_id: str
22
+ tool_call_id: str
23
+ tool_name: str
24
+ message: str
25
+ response_schema: Dict[str, Any]
26
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
27
+ future: asyncio.Future = field(default_factory=lambda: asyncio.get_event_loop().create_future())
28
+
29
+ async def wait_for_response(self, timeout: float = 300.0) -> Dict[str, Any]:
30
+ """
31
+ Wait for the user to respond to the elicitation request.
32
+
33
+ Args:
34
+ timeout: Maximum time to wait in seconds (default 5 minutes)
35
+
36
+ Returns:
37
+ Dict with 'action' and optionally 'data' keys
38
+
39
+ Raises:
40
+ asyncio.TimeoutError: If timeout is reached
41
+ """
42
+ return await asyncio.wait_for(self.future, timeout=timeout)
43
+
44
+
45
+ class ElicitationManager:
46
+ """
47
+ Manages elicitation requests and responses.
48
+
49
+ Provides synchronization between:
50
+ - MCP servers requesting input via ctx.elicit()
51
+ - Frontend UI collecting user responses
52
+ """
53
+
54
+ def __init__(self):
55
+ """Initialize the elicitation manager."""
56
+ self._pending_requests: Dict[str, ElicitationRequest] = {}
57
+ self._lock = asyncio.Lock()
58
+
59
+ def create_elicitation_request(
60
+ self,
61
+ elicitation_id: str,
62
+ tool_call_id: str,
63
+ tool_name: str,
64
+ message: str,
65
+ response_schema: Dict[str, Any]
66
+ ) -> ElicitationRequest:
67
+ """
68
+ Create a new elicitation request.
69
+
70
+ Args:
71
+ elicitation_id: Unique identifier for this elicitation
72
+ tool_call_id: ID of the tool call that requested input
73
+ tool_name: Name of the tool requesting input
74
+ message: Prompt message to display to user
75
+ response_schema: JSON schema defining expected response structure
76
+
77
+ Returns:
78
+ ElicitationRequest object that can be awaited for response
79
+ """
80
+ request = ElicitationRequest(
81
+ elicitation_id=elicitation_id,
82
+ tool_call_id=tool_call_id,
83
+ tool_name=tool_name,
84
+ message=message,
85
+ response_schema=response_schema
86
+ )
87
+ self._pending_requests[elicitation_id] = request
88
+ logger.info(
89
+ f"Created elicitation request: id={elicitation_id}, "
90
+ f"tool={tool_name}, message='{message[:50]}...'"
91
+ )
92
+ return request
93
+
94
+ def handle_elicitation_response(
95
+ self,
96
+ elicitation_id: str,
97
+ action: str,
98
+ data: Optional[Dict[str, Any]] = None
99
+ ) -> bool:
100
+ """
101
+ Handle an elicitation response from the user.
102
+
103
+ Args:
104
+ elicitation_id: ID of the elicitation being responded to
105
+ action: User action - "accept", "decline", or "cancel"
106
+ data: Optional response data (present when action is "accept")
107
+
108
+ Returns:
109
+ True if response was handled, False if request not found
110
+ """
111
+ request = self._pending_requests.get(elicitation_id)
112
+ if not request:
113
+ logger.warning(f"Received response for unknown elicitation: {elicitation_id}")
114
+ return False
115
+
116
+ response = {
117
+ "action": action,
118
+ "data": data
119
+ }
120
+
121
+ if not request.future.done():
122
+ request.future.set_result(response)
123
+ logger.info(
124
+ f"Elicitation response received: id={elicitation_id}, "
125
+ f"action={action}, has_data={data is not None}"
126
+ )
127
+ else:
128
+ logger.warning(
129
+ f"Elicitation response ignored (already resolved): {elicitation_id}"
130
+ )
131
+
132
+ return True
133
+
134
+ def cleanup_request(self, elicitation_id: str) -> None:
135
+ """
136
+ Clean up a completed elicitation request.
137
+
138
+ Args:
139
+ elicitation_id: ID of the request to clean up
140
+ """
141
+ if elicitation_id in self._pending_requests:
142
+ del self._pending_requests[elicitation_id]
143
+ logger.debug(f"Cleaned up elicitation request: {elicitation_id}")
144
+
145
+ def get_pending_request(self, elicitation_id: str) -> Optional[ElicitationRequest]:
146
+ """
147
+ Get a pending elicitation request by ID.
148
+
149
+ Args:
150
+ elicitation_id: ID of the request to retrieve
151
+
152
+ Returns:
153
+ ElicitationRequest if found, None otherwise
154
+ """
155
+ return self._pending_requests.get(elicitation_id)
156
+
157
+ def get_all_pending_requests(self) -> Dict[str, ElicitationRequest]:
158
+ """
159
+ Get all pending elicitation requests.
160
+
161
+ Returns:
162
+ Dictionary mapping elicitation IDs to requests
163
+ """
164
+ return dict(self._pending_requests)
165
+
166
+ def cancel_all_requests(self) -> None:
167
+ """Cancel all pending elicitation requests."""
168
+ for request in self._pending_requests.values():
169
+ if not request.future.done():
170
+ request.future.set_exception(
171
+ asyncio.CancelledError("Elicitation cancelled")
172
+ )
173
+ self._pending_requests.clear()
174
+ logger.info("Cancelled all pending elicitation requests")
175
+
176
+
177
+ # Global singleton instance
178
+ _elicitation_manager: Optional[ElicitationManager] = None
179
+
180
+
181
+ def get_elicitation_manager() -> ElicitationManager:
182
+ """
183
+ Get the global elicitation manager singleton.
184
+
185
+ Returns:
186
+ Global ElicitationManager instance
187
+ """
188
+ global _elicitation_manager
189
+ if _elicitation_manager is None:
190
+ _elicitation_manager = ElicitationManager()
191
+ return _elicitation_manager
@@ -0,0 +1 @@
1
+ """Event modules for chat application."""
@@ -0,0 +1,112 @@
1
+ """Agent event relay - maps AgentEvents to EventPublisher calls."""
2
+
3
+ import logging
4
+ from typing import Any, Awaitable, Callable, Optional
5
+
6
+ from atlas.interfaces.events import EventPublisher
7
+
8
+ from ..agent.protocols import AgentEvent
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # Constants
13
+ UNKNOWN_TOOL_NAME = "unknown"
14
+
15
+
16
+ class AgentEventRelay:
17
+ """
18
+ Translates agent loop events to UI update events.
19
+
20
+ Maps AgentEvent instances to appropriate EventPublisher method calls,
21
+ providing a clean separation between agent logic and UI transport.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ event_publisher: EventPublisher,
27
+ artifact_processor: Optional[Callable[[Any], Awaitable[None]]] = None,
28
+ ):
29
+ """
30
+ Initialize agent event relay.
31
+
32
+ Args:
33
+ event_publisher: Publisher for sending UI updates
34
+ artifact_processor: Optional callback for processing tool artifacts
35
+ """
36
+ self.event_publisher = event_publisher
37
+ self.artifact_processor = artifact_processor
38
+
39
+ async def handle_event(self, evt: AgentEvent) -> None:
40
+ """
41
+ Handle an agent event and relay it to the UI.
42
+
43
+ Args:
44
+ evt: Agent event to handle
45
+ """
46
+ et = evt.type
47
+ p = evt.payload or {}
48
+
49
+ # Map event types to publisher calls
50
+ if et == "agent_start":
51
+ await self.event_publisher.publish_agent_update(
52
+ update_type="agent_start",
53
+ max_steps=p.get("max_steps"),
54
+ strategy=p.get("strategy"),
55
+ )
56
+
57
+ elif et == "agent_turn_start":
58
+ await self.event_publisher.publish_agent_update(
59
+ update_type="agent_turn_start",
60
+ step=p.get("step"),
61
+ )
62
+
63
+ elif et == "agent_reason":
64
+ await self.event_publisher.publish_agent_update(
65
+ update_type="agent_reason",
66
+ message=p.get("message"),
67
+ step=p.get("step"),
68
+ )
69
+
70
+ elif et == "agent_request_input":
71
+ await self.event_publisher.publish_agent_update(
72
+ update_type="agent_request_input",
73
+ question=p.get("question"),
74
+ step=p.get("step"),
75
+ )
76
+
77
+ elif et == "agent_tool_start":
78
+ await self.event_publisher.publish_tool_start(
79
+ tool_name=p.get("tool", UNKNOWN_TOOL_NAME),
80
+ )
81
+
82
+ elif et == "agent_tool_complete":
83
+ await self.event_publisher.publish_tool_complete(
84
+ tool_name=p.get("tool", UNKNOWN_TOOL_NAME),
85
+ result=p.get("result"),
86
+ )
87
+
88
+ elif et == "agent_tool_results":
89
+ # Delegate artifact processing to external handler
90
+ if self.artifact_processor:
91
+ results = p.get("results") or []
92
+ if results:
93
+ await self.artifact_processor(results)
94
+
95
+ elif et == "agent_observe":
96
+ await self.event_publisher.publish_agent_update(
97
+ update_type="agent_observe",
98
+ message=p.get("message"),
99
+ step=p.get("step"),
100
+ )
101
+
102
+ elif et == "agent_completion":
103
+ await self.event_publisher.publish_agent_update(
104
+ update_type="agent_completion",
105
+ steps=p.get("steps"),
106
+ )
107
+
108
+ elif et == "agent_error":
109
+ await self.event_publisher.publish_agent_update(
110
+ update_type="agent_error",
111
+ message=p.get("message"),
112
+ )
@@ -0,0 +1 @@
1
+ """Mode runner modules for different chat execution modes."""
@@ -0,0 +1,125 @@
1
+ """Agent mode runner - handles LLM calls with agent loop 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
+
10
+ from ..agent import AgentLoopFactory
11
+ from ..agent.protocols import AgentContext
12
+ from ..events.agent_event_relay import AgentEventRelay
13
+ from ..utilities import event_notifier
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Type hint for the update callback
18
+ UpdateCallback = Callable[[Dict[str, Any]], Awaitable[None]]
19
+
20
+
21
+ class AgentModeRunner:
22
+ """
23
+ Runner for agent mode.
24
+
25
+ Executes agent loops with event streaming and artifact processing.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ agent_loop_factory: AgentLoopFactory,
31
+ event_publisher: EventPublisher,
32
+ artifact_processor: Optional[Callable[[Session, List[ToolResult], Optional[UpdateCallback]], Awaitable[None]]] = None,
33
+ default_strategy: str = "think-act",
34
+ ):
35
+ """
36
+ Initialize agent mode runner.
37
+
38
+ Args:
39
+ agent_loop_factory: Factory for creating agent loops
40
+ event_publisher: Event publisher for UI updates
41
+ artifact_processor: Optional callback for processing tool artifacts
42
+ default_strategy: Default agent loop strategy
43
+ """
44
+ self.agent_loop_factory = agent_loop_factory
45
+ self.event_publisher = event_publisher
46
+ self.artifact_processor = artifact_processor
47
+ self.default_strategy = default_strategy
48
+
49
+ async def run(
50
+ self,
51
+ session: Session,
52
+ model: str,
53
+ messages: List[Dict[str, Any]],
54
+ selected_tools: Optional[List[str]],
55
+ selected_data_sources: Optional[List[str]],
56
+ max_steps: int,
57
+ temperature: float = 0.7,
58
+ agent_loop_strategy: Optional[str] = None,
59
+ ) -> Dict[str, Any]:
60
+ """
61
+ Execute agent mode.
62
+
63
+ Args:
64
+ session: Current chat session
65
+ model: LLM model to use
66
+ messages: Message history
67
+ selected_tools: Optional list of tools to make available
68
+ selected_data_sources: Optional list of data sources
69
+ max_steps: Maximum number of agent steps
70
+ temperature: LLM temperature parameter
71
+ agent_loop_strategy: Strategy name (react, think-act). Falls back to default.
72
+
73
+ Returns:
74
+ Response dictionary
75
+ """
76
+ # Get agent loop from factory based on strategy
77
+ strategy = agent_loop_strategy or self.default_strategy
78
+ agent_loop = self.agent_loop_factory.create(strategy)
79
+
80
+ # Build agent context
81
+ agent_context = AgentContext(
82
+ session_id=session.id,
83
+ user_email=session.user_email,
84
+ files=session.context.get("files", {}),
85
+ history=session.history,
86
+ )
87
+
88
+ # Artifact processor wrapper for handling tool results
89
+ async def process_artifacts(results):
90
+ if self.artifact_processor:
91
+ await self.artifact_processor(session, results, None)
92
+
93
+ # Create event relay to map AgentEvents to UI updates
94
+ event_relay = AgentEventRelay(
95
+ event_publisher=self.event_publisher,
96
+ artifact_processor=process_artifacts,
97
+ )
98
+
99
+ # Run the loop
100
+ result = await agent_loop.run(
101
+ model=model,
102
+ messages=messages,
103
+ context=agent_context,
104
+ selected_tools=selected_tools,
105
+ data_sources=selected_data_sources,
106
+ max_steps=max_steps,
107
+ temperature=temperature,
108
+ event_handler=event_relay.handle_event,
109
+ )
110
+
111
+ # Append final message
112
+ assistant_message = Message(
113
+ role=MessageRole.ASSISTANT,
114
+ content=result.final_answer,
115
+ metadata={"agent_mode": True, "steps": result.steps},
116
+ )
117
+ session.history.add_message(assistant_message)
118
+
119
+ # Completion update
120
+ await self.event_publisher.publish_agent_update(
121
+ update_type="agent_completion",
122
+ steps=result.steps
123
+ )
124
+
125
+ return event_notifier.create_chat_response(result.final_answer)
@@ -0,0 +1,74 @@
1
+ """Plain mode runner - handles simple LLM calls without tools or RAG."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, List
5
+
6
+ from atlas.domain.messages.models import Message, MessageRole
7
+ from atlas.domain.sessions.models import Session
8
+ from atlas.interfaces.events import EventPublisher
9
+ from atlas.interfaces.llm import LLMProtocol
10
+
11
+ from ..utilities import event_notifier
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class PlainModeRunner:
17
+ """
18
+ Runner for plain LLM mode.
19
+
20
+ Executes simple LLM calls without tools or RAG integration.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ llm: LLMProtocol,
26
+ event_publisher: EventPublisher,
27
+ ):
28
+ """
29
+ Initialize plain mode runner.
30
+
31
+ Args:
32
+ llm: LLM protocol implementation
33
+ event_publisher: Event publisher for UI updates
34
+ """
35
+ self.llm = llm
36
+ self.event_publisher = event_publisher
37
+
38
+ async def run(
39
+ self,
40
+ session: Session,
41
+ model: str,
42
+ messages: List[Dict[str, str]],
43
+ temperature: float = 0.7,
44
+ ) -> Dict[str, Any]:
45
+ """
46
+ Execute plain LLM mode.
47
+
48
+ Args:
49
+ session: Current chat session
50
+ model: LLM model to use
51
+ messages: Message history
52
+ temperature: LLM temperature parameter
53
+
54
+ Returns:
55
+ Response dictionary
56
+ """
57
+ # Call LLM
58
+ response_content = await self.llm.call_plain(model, messages, temperature=temperature)
59
+
60
+ # Add assistant message to history
61
+ assistant_message = Message(
62
+ role=MessageRole.ASSISTANT,
63
+ content=response_content
64
+ )
65
+ session.history.add_message(assistant_message)
66
+
67
+ # Publish events
68
+ await self.event_publisher.publish_chat_response(
69
+ message=response_content,
70
+ has_pending_tools=False,
71
+ )
72
+ await self.event_publisher.publish_response_complete()
73
+
74
+ return event_notifier.create_chat_response(response_content)
@@ -0,0 +1,81 @@
1
+ """RAG mode runner - handles LLM calls with RAG integration."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, List
5
+
6
+ from atlas.domain.messages.models import Message, MessageRole
7
+ from atlas.domain.sessions.models import Session
8
+ from atlas.interfaces.events import EventPublisher
9
+ from atlas.interfaces.llm import LLMProtocol
10
+
11
+ from ..utilities import event_notifier
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class RagModeRunner:
17
+ """
18
+ Runner for RAG mode.
19
+
20
+ Executes LLM calls with Retrieval-Augmented Generation integration.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ llm: LLMProtocol,
26
+ event_publisher: EventPublisher,
27
+ ):
28
+ """
29
+ Initialize RAG mode runner.
30
+
31
+ Args:
32
+ llm: LLM protocol implementation
33
+ event_publisher: Event publisher for UI updates
34
+ """
35
+ self.llm = llm
36
+ self.event_publisher = event_publisher
37
+
38
+ async def run(
39
+ self,
40
+ session: Session,
41
+ model: str,
42
+ messages: List[Dict[str, str]],
43
+ data_sources: List[str],
44
+ user_email: str,
45
+ temperature: float = 0.7,
46
+ ) -> Dict[str, Any]:
47
+ """
48
+ Execute RAG mode.
49
+
50
+ Args:
51
+ session: Current chat session
52
+ model: LLM model to use
53
+ messages: Message history
54
+ data_sources: List of data sources to query
55
+ user_email: User email for authorization
56
+ temperature: LLM temperature parameter
57
+
58
+ Returns:
59
+ Response dictionary
60
+ """
61
+ # Call LLM with RAG
62
+ response_content = await self.llm.call_with_rag(
63
+ model, messages, data_sources, user_email, temperature=temperature
64
+ )
65
+
66
+ # Add assistant message to history
67
+ assistant_message = Message(
68
+ role=MessageRole.ASSISTANT,
69
+ content=response_content,
70
+ metadata={"data_sources": data_sources}
71
+ )
72
+ session.history.add_message(assistant_message)
73
+
74
+ # Publish events
75
+ await self.event_publisher.publish_chat_response(
76
+ message=response_content,
77
+ has_pending_tools=False,
78
+ )
79
+ await self.event_publisher.publish_response_complete()
80
+
81
+ return event_notifier.create_chat_response(response_content)