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,140 @@
1
+ """CLI event publisher for headless/non-interactive use."""
2
+
3
+ import logging
4
+ import sys
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Dict, List, Optional
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ @dataclass
12
+ class CLICollectedResult:
13
+ """Structured result from a collected CLI chat session."""
14
+
15
+ message: str = ""
16
+ tool_calls: List[Dict[str, Any]] = field(default_factory=list)
17
+ files: Dict[str, Any] = field(default_factory=dict)
18
+ canvas_content: Optional[str] = None
19
+ raw_events: List[Dict[str, Any]] = field(default_factory=list)
20
+
21
+
22
+ class CLIEventPublisher:
23
+ """
24
+ Event publisher for CLI / headless usage.
25
+
26
+ Two modes:
27
+ - streaming: prints token text to stdout, tool/status info to stderr
28
+ - collecting: buffers all events, returns structured result
29
+ """
30
+
31
+ def __init__(self, streaming: bool = True, quiet: bool = False):
32
+ self.streaming = streaming
33
+ self.quiet = quiet
34
+ self._collected = CLICollectedResult()
35
+
36
+ def get_result(self) -> CLICollectedResult:
37
+ """Return the collected result (useful in collecting mode)."""
38
+ return self._collected
39
+
40
+ async def publish_chat_response(
41
+ self,
42
+ message: str,
43
+ has_pending_tools: bool = False,
44
+ ) -> None:
45
+ self._collected.message += message
46
+ if self.streaming:
47
+ sys.stdout.write(message)
48
+ sys.stdout.flush()
49
+
50
+ async def publish_response_complete(self) -> None:
51
+ if self.streaming:
52
+ # Ensure final newline
53
+ sys.stdout.write("\n")
54
+ sys.stdout.flush()
55
+
56
+ async def publish_agent_update(
57
+ self,
58
+ update_type: str,
59
+ **kwargs: Any,
60
+ ) -> None:
61
+ event = {"type": "agent_update", "update_type": update_type, **kwargs}
62
+ self._collected.raw_events.append(event)
63
+ if self.streaming and not self.quiet:
64
+ _print_status(f"[agent] {update_type}")
65
+
66
+ async def publish_tool_start(
67
+ self,
68
+ tool_name: str,
69
+ **kwargs: Any,
70
+ ) -> None:
71
+ self._collected.tool_calls.append({"tool": tool_name, "status": "started"})
72
+ if self.streaming and not self.quiet:
73
+ _print_status(f"[tool] {tool_name} ...")
74
+
75
+ async def publish_tool_complete(
76
+ self,
77
+ tool_name: str,
78
+ result: Any,
79
+ **kwargs: Any,
80
+ ) -> None:
81
+ # Update last matching tool call
82
+ for tc in reversed(self._collected.tool_calls):
83
+ if tc["tool"] == tool_name and tc["status"] == "started":
84
+ tc["status"] = "complete"
85
+ tc["result"] = result
86
+ break
87
+ if self.streaming and not self.quiet:
88
+ _print_status(f"[tool] {tool_name} done")
89
+
90
+ async def publish_files_update(
91
+ self,
92
+ files: Dict[str, Any],
93
+ ) -> None:
94
+ self._collected.files.update(files)
95
+ if self.streaming and not self.quiet:
96
+ _print_status(f"[files] {len(files)} file(s)")
97
+
98
+ async def publish_canvas_content(
99
+ self,
100
+ content: str,
101
+ content_type: str = "text/html",
102
+ **kwargs: Any,
103
+ ) -> None:
104
+ self._collected.canvas_content = content
105
+
106
+ async def send_json(self, data: Dict[str, Any]) -> None:
107
+ self._collected.raw_events.append(data)
108
+ if self.streaming and not self.quiet:
109
+ msg_type = data.get("type", "")
110
+ if msg_type == "tool_start":
111
+ tool_name = data.get("tool_name", "unknown")
112
+ args = data.get("arguments", {})
113
+ _print_status(f"[tool] {tool_name} called with: {args}")
114
+ elif msg_type == "tool_complete":
115
+ tool_name = data.get("tool_name", "unknown")
116
+ success = data.get("success", False)
117
+ result = data.get("result", "")
118
+ status = "ok" if success else "error"
119
+ _print_status(f"[tool] {tool_name} {status}: {result}")
120
+
121
+ async def publish_elicitation_request(
122
+ self,
123
+ elicitation_id: str,
124
+ tool_call_id: str,
125
+ tool_name: str,
126
+ message: str,
127
+ response_schema: Dict[str, Any],
128
+ ) -> None:
129
+ # CLI cannot handle interactive elicitation; log and skip
130
+ logger.warning(
131
+ "Elicitation requested by tool %s but CLI mode cannot respond interactively",
132
+ tool_name,
133
+ )
134
+ if self.streaming and not self.quiet:
135
+ _print_status(f"[elicitation] {tool_name}: {message} (skipped, non-interactive)")
136
+
137
+
138
+ def _print_status(text: str) -> None:
139
+ """Print status/tool info to stderr so stdout stays clean for LLM output."""
140
+ print(text, file=sys.stderr, flush=True)
@@ -0,0 +1,140 @@
1
+ """WebSocket-based event publisher implementation."""
2
+
3
+ import logging
4
+ from typing import Any, Dict, Optional
5
+
6
+ from atlas.application.chat.utilities import event_notifier
7
+ from atlas.interfaces.transport import ChatConnectionProtocol
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class WebSocketEventPublisher:
13
+ """
14
+ WebSocket implementation of EventPublisher.
15
+
16
+ Wraps event_notifier and ChatConnectionProtocol to publish
17
+ events to connected WebSocket clients.
18
+ """
19
+
20
+ def __init__(self, connection: Optional[ChatConnectionProtocol] = None):
21
+ """
22
+ Initialize WebSocket event publisher.
23
+
24
+ Args:
25
+ connection: WebSocket connection for sending messages
26
+ """
27
+ self.connection = connection
28
+
29
+ async def publish_chat_response(
30
+ self,
31
+ message: str,
32
+ has_pending_tools: bool = False,
33
+ ) -> None:
34
+ """Publish a chat response message."""
35
+ if self.connection:
36
+ await event_notifier.notify_chat_response(
37
+ message=message,
38
+ has_pending_tools=has_pending_tools,
39
+ update_callback=self.connection.send_json,
40
+ )
41
+
42
+ async def publish_response_complete(self) -> None:
43
+ """Signal that the response is complete."""
44
+ if self.connection:
45
+ await event_notifier.notify_response_complete(
46
+ self.connection.send_json
47
+ )
48
+
49
+ async def publish_agent_update(
50
+ self,
51
+ update_type: str,
52
+ **kwargs: Any
53
+ ) -> None:
54
+ """Publish an agent-specific update."""
55
+ if self.connection:
56
+ await event_notifier.notify_agent_update(
57
+ update_type=update_type,
58
+ connection=self.connection,
59
+ **kwargs
60
+ )
61
+
62
+ async def publish_tool_start(
63
+ self,
64
+ tool_name: str,
65
+ **kwargs: Any
66
+ ) -> None:
67
+ """Publish notification that a tool is starting."""
68
+ if self.connection:
69
+ await event_notifier.notify_agent_update(
70
+ update_type="tool_start",
71
+ connection=self.connection,
72
+ tool=tool_name,
73
+ **kwargs
74
+ )
75
+
76
+ async def publish_tool_complete(
77
+ self,
78
+ tool_name: str,
79
+ result: Any,
80
+ **kwargs: Any
81
+ ) -> None:
82
+ """Publish notification that a tool has completed."""
83
+ if self.connection:
84
+ await event_notifier.notify_agent_update(
85
+ update_type="tool_complete",
86
+ connection=self.connection,
87
+ tool=tool_name,
88
+ result=result,
89
+ **kwargs
90
+ )
91
+
92
+ async def publish_files_update(
93
+ self,
94
+ files: Dict[str, Any]
95
+ ) -> None:
96
+ """Publish update about session files."""
97
+ if self.connection:
98
+ await self.connection.send_json({
99
+ "type": "files_update",
100
+ "files": files
101
+ })
102
+
103
+ async def publish_canvas_content(
104
+ self,
105
+ content: str,
106
+ content_type: str = "text/html",
107
+ **kwargs: Any
108
+ ) -> None:
109
+ """Publish content for canvas display."""
110
+ if self.connection:
111
+ await self.connection.send_json({
112
+ "type": "canvas_content",
113
+ "content": content,
114
+ "content_type": content_type,
115
+ **kwargs
116
+ })
117
+
118
+ async def send_json(self, data: Dict[str, Any]) -> None:
119
+ """Send raw JSON message."""
120
+ if self.connection:
121
+ await self.connection.send_json(data)
122
+
123
+ async def publish_elicitation_request(
124
+ self,
125
+ elicitation_id: str,
126
+ tool_call_id: str,
127
+ tool_name: str,
128
+ message: str,
129
+ response_schema: Dict[str, Any]
130
+ ) -> None:
131
+ """Publish an elicitation request to the user."""
132
+ if self.connection:
133
+ await self.connection.send_json({
134
+ "type": "elicitation_request",
135
+ "elicitation_id": elicitation_id,
136
+ "tool_call_id": tool_call_id,
137
+ "tool_name": tool_name,
138
+ "message": message,
139
+ "response_schema": response_schema
140
+ })
@@ -0,0 +1,56 @@
1
+ """In-memory session repository implementation."""
2
+
3
+ import logging
4
+ from typing import Dict, Optional
5
+ from uuid import UUID
6
+
7
+ from atlas.domain.errors import SessionNotFoundError
8
+ from atlas.domain.sessions.models import Session
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class InMemorySessionRepository:
14
+ """
15
+ In-memory implementation of SessionRepository.
16
+
17
+ Stores sessions in a dictionary. Suitable for single-instance deployments
18
+ or testing. For distributed systems, use Redis or database-backed implementation.
19
+ """
20
+
21
+ def __init__(self):
22
+ """Initialize empty session storage."""
23
+ self._sessions: Dict[UUID, Session] = {}
24
+
25
+ async def get(self, session_id: UUID) -> Optional[Session]:
26
+ """Retrieve a session by ID."""
27
+ return self._sessions.get(session_id)
28
+
29
+ async def create(self, session: Session) -> Session:
30
+ """Create and store a new session."""
31
+ self._sessions[session.id] = session
32
+ logger.info(f"Created session {session.id} for user {session.user_email}")
33
+ return session
34
+
35
+ async def update(self, session: Session) -> Session:
36
+ """Update an existing session."""
37
+ if session.id not in self._sessions:
38
+ raise SessionNotFoundError(
39
+ f"Session {session.id} not found",
40
+ code="SESSION_NOT_FOUND"
41
+ )
42
+ self._sessions[session.id] = session
43
+ return session
44
+
45
+ async def delete(self, session_id: UUID) -> bool:
46
+ """Delete a session."""
47
+ if session_id in self._sessions:
48
+ self._sessions[session_id].active = False
49
+ logger.info(f"Deleted session {session_id}")
50
+ del self._sessions[session_id]
51
+ return True
52
+ return False
53
+
54
+ async def exists(self, session_id: UUID) -> bool:
55
+ """Check if a session exists."""
56
+ return session_id in self._sessions
@@ -0,0 +1,7 @@
1
+ """Transport adapters."""
2
+
3
+ from .websocket_connection_adapter import WebSocketConnectionAdapter
4
+
5
+ __all__ = [
6
+ "WebSocketConnectionAdapter",
7
+ ]
@@ -0,0 +1,33 @@
1
+ """WebSocket connection adapter implementing ChatConnectionProtocol."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from fastapi import WebSocket
6
+
7
+
8
+ class WebSocketConnectionAdapter:
9
+ """
10
+ Adapter that wraps FastAPI WebSocket to implement ChatConnectionProtocol.
11
+ This isolates the application layer from FastAPI-specific types.
12
+ """
13
+
14
+ def __init__(self, websocket: WebSocket, user_email: Optional[str] = None):
15
+ """Initialize with FastAPI WebSocket and associated user."""
16
+ self.websocket = websocket
17
+ self.user_email = user_email
18
+
19
+ async def send_json(self, data: Dict[str, Any]) -> None:
20
+ """Send JSON data to the client."""
21
+ await self.websocket.send_json(data)
22
+
23
+ async def receive_json(self) -> Dict[str, Any]:
24
+ """Receive JSON data from the client."""
25
+ return await self.websocket.receive_json()
26
+
27
+ async def accept(self) -> None:
28
+ """Accept the connection."""
29
+ await self.websocket.accept()
30
+
31
+ async def close(self) -> None:
32
+ """Close the connection."""
33
+ await self.websocket.close()
atlas/init_cli.py ADDED
@@ -0,0 +1,226 @@
1
+ """
2
+ Atlas Init CLI - Set up configuration files for Atlas.
3
+
4
+ Usage:
5
+ atlas-init # Interactive setup in current directory
6
+ atlas-init --target ./myapp # Setup in specific directory
7
+ atlas-init --minimal # Create minimal .env only
8
+ atlas-init --force # Overwrite existing files without prompting
9
+ """
10
+
11
+ import argparse
12
+ import shutil
13
+ import sys
14
+ from pathlib import Path
15
+
16
+
17
+ def get_package_root() -> Path:
18
+ """Get the root directory of the atlas package."""
19
+ return Path(__file__).resolve().parent.parent
20
+
21
+
22
+ def get_config_defaults_dir() -> Path:
23
+ """Get the path to config/defaults in the package."""
24
+ return get_package_root() / "config" / "defaults"
25
+
26
+
27
+ def get_env_example_path() -> Path:
28
+ """Get the path to .env.example in the package."""
29
+ return get_package_root() / ".env.example"
30
+
31
+
32
+ def prompt_yes_no(message: str, default: bool = False) -> bool:
33
+ """Prompt user for yes/no confirmation."""
34
+ suffix = " [Y/n]: " if default else " [y/N]: "
35
+ while True:
36
+ response = input(message + suffix).strip().lower()
37
+ if response == "":
38
+ return default
39
+ if response in ("y", "yes"):
40
+ return True
41
+ if response in ("n", "no"):
42
+ return False
43
+ print("Please enter 'y' or 'n'")
44
+
45
+
46
+ def copy_with_prompt(src: Path, dst: Path, force: bool = False) -> bool:
47
+ """Copy a file, prompting if destination exists."""
48
+ if dst.exists() and not force:
49
+ if not prompt_yes_no(f" {dst} already exists. Overwrite?"):
50
+ print(f" Skipping {dst.name}")
51
+ return False
52
+
53
+ if src.is_dir():
54
+ if dst.exists():
55
+ shutil.rmtree(dst)
56
+ shutil.copytree(src, dst)
57
+ else:
58
+ shutil.copy2(src, dst)
59
+
60
+ print(f" Created {dst}")
61
+ return True
62
+
63
+
64
+ def create_minimal_env(target_dir: Path, force: bool = False) -> bool:
65
+ """Create a minimal .env file with just API key placeholders."""
66
+ env_path = target_dir / ".env"
67
+
68
+ if env_path.exists() and not force:
69
+ if not prompt_yes_no(f" {env_path} already exists. Overwrite?"):
70
+ print(f" Skipping {env_path.name}")
71
+ return False
72
+
73
+ minimal_env = """\
74
+ # Atlas Configuration
75
+ # See https://github.com/sandialabs/atlas-ui-3 for full documentation
76
+
77
+ # =============================================================================
78
+ # LLM API Keys (set at least one)
79
+ # =============================================================================
80
+ OPENAI_API_KEY=your-openai-api-key-here
81
+ ANTHROPIC_API_KEY=your-anthropic-api-key-here
82
+ # GOOGLE_API_KEY=your-google-api-key-here
83
+
84
+ # =============================================================================
85
+ # Server Settings
86
+ # =============================================================================
87
+ PORT=8000
88
+ DEBUG_MODE=true
89
+
90
+ # =============================================================================
91
+ # Optional: Custom config location
92
+ # Uncomment and set if you have custom config files
93
+ # =============================================================================
94
+ # APP_CONFIG_OVERRIDES=./config
95
+
96
+ # =============================================================================
97
+ # Optional: RAG Configuration
98
+ # =============================================================================
99
+ # FEATURE_RAG_ENABLED=false
100
+ # ATLAS_RAG_URL=https://your-rag-api.example.com
101
+ # ATLAS_RAG_BEARER_TOKEN=your-api-key-here
102
+ """
103
+
104
+ env_path.write_text(minimal_env)
105
+ print(f" Created {env_path}")
106
+ return True
107
+
108
+
109
+ def run_init(args: argparse.Namespace) -> int:
110
+ """Run the atlas-init command."""
111
+ target_dir = Path(args.target).resolve()
112
+
113
+ # Ensure target directory exists
114
+ if not target_dir.exists():
115
+ if not args.force:
116
+ if not prompt_yes_no(f"Directory {target_dir} does not exist. Create it?", default=True):
117
+ print("Aborted.")
118
+ return 1
119
+ target_dir.mkdir(parents=True, exist_ok=True)
120
+ print(f"Created directory: {target_dir}")
121
+
122
+ print(f"\nSetting up Atlas configuration in: {target_dir}\n")
123
+
124
+ config_defaults = get_config_defaults_dir()
125
+ env_example = get_env_example_path()
126
+
127
+ if args.minimal:
128
+ # Minimal mode: just create a simple .env
129
+ print("Creating minimal configuration...")
130
+ create_minimal_env(target_dir, force=args.force)
131
+ else:
132
+ # Full mode: copy config and .env
133
+ print("Copying configuration files...")
134
+
135
+ # Copy config/defaults to target/config
136
+ if config_defaults.exists():
137
+ target_config = target_dir / "config"
138
+ copy_with_prompt(config_defaults, target_config, force=args.force)
139
+ else:
140
+ print(f" Warning: Config defaults not found at {config_defaults}")
141
+
142
+ # Copy .env.example to .env
143
+ if env_example.exists():
144
+ target_env = target_dir / ".env"
145
+ if copy_with_prompt(env_example, target_env, force=args.force):
146
+ print("\n Remember to edit .env and add your API keys!")
147
+ else:
148
+ # Fall back to creating minimal env
149
+ print(" .env.example not found, creating minimal .env...")
150
+ create_minimal_env(target_dir, force=args.force)
151
+
152
+ print("\n" + "=" * 60)
153
+ print("Setup complete!")
154
+ print("=" * 60)
155
+ print("\nNext steps:")
156
+ print(f" 1. Edit {target_dir / '.env'} and add your API keys")
157
+ print(" 2. Run: atlas-chat 'Hello, world!'")
158
+ print(" 3. Or start the server: atlas-server")
159
+
160
+ if not args.minimal:
161
+ print(f"\nConfig files are in: {target_dir / 'config'}")
162
+ print(" - llmconfig.yml: LLM model configurations")
163
+ print(" - mcp.json: MCP tool server configurations")
164
+
165
+ print()
166
+ return 0
167
+
168
+
169
+ def build_parser() -> argparse.ArgumentParser:
170
+ """Build argument parser for atlas-init CLI."""
171
+ parser = argparse.ArgumentParser(
172
+ prog="atlas-init",
173
+ description="Set up Atlas configuration files in your project directory.",
174
+ formatter_class=argparse.RawDescriptionHelpFormatter,
175
+ epilog="""
176
+ Examples:
177
+ atlas-init Set up config in current directory
178
+ atlas-init --target ./myapp Set up config in ./myapp
179
+ atlas-init --minimal Create only a minimal .env file
180
+ atlas-init --force Overwrite existing files without prompting
181
+
182
+ After running atlas-init, edit the .env file to add your API keys.
183
+ """,
184
+ )
185
+ parser.add_argument(
186
+ "--target",
187
+ "-t",
188
+ default=".",
189
+ help="Target directory for configuration files (default: current directory).",
190
+ )
191
+ parser.add_argument(
192
+ "--minimal",
193
+ "-m",
194
+ action="store_true",
195
+ help="Create only a minimal .env file (no config folder).",
196
+ )
197
+ parser.add_argument(
198
+ "--force",
199
+ "-f",
200
+ action="store_true",
201
+ help="Overwrite existing files without prompting.",
202
+ )
203
+ parser.add_argument(
204
+ "--version",
205
+ action="store_true",
206
+ help="Print version and exit.",
207
+ )
208
+ return parser
209
+
210
+
211
+ def main() -> None:
212
+ """Main entry point for atlas-init CLI."""
213
+ parser = build_parser()
214
+ args = parser.parse_args()
215
+
216
+ if args.version:
217
+ from atlas.version import VERSION
218
+
219
+ print(f"atlas-init version {VERSION}")
220
+ sys.exit(0)
221
+
222
+ sys.exit(run_init(args))
223
+
224
+
225
+ if __name__ == "__main__":
226
+ main()
@@ -0,0 +1,15 @@
1
+ """Interfaces layer - protocols and contracts."""
2
+
3
+ from .llm import LLMProtocol, LLMResponse
4
+ from .rag import RAGClientProtocol
5
+ from .tools import ToolManagerProtocol, ToolProtocol
6
+ from .transport import ChatConnectionProtocol
7
+
8
+ __all__ = [
9
+ "LLMProtocol",
10
+ "LLMResponse",
11
+ "RAGClientProtocol",
12
+ "ToolProtocol",
13
+ "ToolManagerProtocol",
14
+ "ChatConnectionProtocol",
15
+ ]