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,347 @@
1
+ """
2
+ Non-interactive CLI for Atlas chat.
3
+
4
+ Usage:
5
+ python atlas_chat_cli.py "Summarize the latest docs" --model gpt-4o
6
+ python atlas_chat_cli.py "Use the search tool" --tools server_tool1
7
+ python atlas_chat_cli.py --list-tools
8
+ echo "prompt" | python atlas_chat_cli.py - --model gpt-4o
9
+ python atlas_chat_cli.py "prompt" --env-file /path/to/custom.env
10
+ """
11
+
12
+ import argparse
13
+ import asyncio
14
+ import json
15
+ import logging
16
+ import os
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ # Suppress LiteLLM verbose stdout noise BEFORE any transitive import of litellm.
21
+ # litellm._logging reads LITELLM_LOG at import time and defaults to DEBUG.
22
+ if "LITELLM_LOG" not in os.environ:
23
+ os.environ["LITELLM_LOG"] = "ERROR"
24
+
25
+ from dotenv import load_dotenv
26
+
27
+
28
+ # Phase 1: Parse --env-file early, before loading env and importing atlas code.
29
+ # This allows specifying a custom .env file that affects all subsequent imports.
30
+ def _get_env_file_from_args() -> tuple[Path, bool]:
31
+ """Extract --env-file from sys.argv without full parsing.
32
+
33
+ Returns:
34
+ Tuple of (env_path, is_custom) where is_custom is True if user
35
+ explicitly provided --env-file, False for default .env path.
36
+ """
37
+ default_env = Path(__file__).resolve().parents[1] / ".env"
38
+ for i, arg in enumerate(sys.argv[1:], start=1):
39
+ if arg == "--env-file" and i + 1 < len(sys.argv):
40
+ return Path(sys.argv[i + 1]), True
41
+ if arg.startswith("--env-file="):
42
+ return Path(arg.split("=", 1)[1]), True
43
+ return default_env, False
44
+
45
+
46
+ def _extract_flag_value(argv: list[str], flag_name: str) -> str | None:
47
+ """Extract a flag value from argv.
48
+
49
+ Supports both `--flag value` and `--flag=value` forms.
50
+ """
51
+ for i, arg in enumerate(argv):
52
+ if arg == flag_name and i + 1 < len(argv):
53
+ return argv[i + 1]
54
+ if arg.startswith(flag_name + "="):
55
+ return arg.split("=", 1)[1]
56
+ return None
57
+
58
+
59
+ def _apply_config_overrides_from_args() -> None:
60
+ """Apply config path overrides from CLI args as env vars.
61
+
62
+ This must run BEFORE load_dotenv() and BEFORE importing atlas code, so
63
+ flags override values coming from .env files.
64
+ """
65
+ argv = sys.argv[1:]
66
+
67
+ # Directories
68
+ overrides_dir = _extract_flag_value(argv, "--config-overrides")
69
+ defaults_dir = _extract_flag_value(argv, "--config-defaults")
70
+ if overrides_dir:
71
+ os.environ["APP_CONFIG_OVERRIDES"] = str(Path(overrides_dir).expanduser().resolve())
72
+ if defaults_dir:
73
+ os.environ["APP_CONFIG_DEFAULTS"] = str(Path(defaults_dir).expanduser().resolve())
74
+
75
+ def _apply_config_file_override(flag: str, env_var: str) -> None:
76
+ value = _extract_flag_value(argv, flag)
77
+ if not value:
78
+ return
79
+ p = Path(value).expanduser()
80
+ # If user provides a path, set *_CONFIG_FILE to basename and (unless
81
+ # explicitly set) point APP_CONFIG_OVERRIDES at the containing directory.
82
+ if "/" in value or p.parent != Path("."):
83
+ resolved = p.resolve()
84
+ os.environ[env_var] = resolved.name
85
+ if "APP_CONFIG_OVERRIDES" not in os.environ:
86
+ os.environ["APP_CONFIG_OVERRIDES"] = str(resolved.parent)
87
+ else:
88
+ os.environ[env_var] = value
89
+
90
+ # Individual config files
91
+ _apply_config_file_override("--mcp-config", "MCP_CONFIG_FILE")
92
+ _apply_config_file_override("--rag-sources-config", "RAG_SOURCES_CONFIG_FILE")
93
+ _apply_config_file_override("--llm-config", "LLM_CONFIG_FILE")
94
+ _apply_config_file_override("--help-config", "HELP_CONFIG_FILE")
95
+ _apply_config_file_override("--messages-config", "MESSAGES_CONFIG_FILE")
96
+ _apply_config_file_override("--tool-approvals-config", "TOOL_APPROVALS_CONFIG_FILE")
97
+ _apply_config_file_override("--splash-config", "SPLASH_CONFIG_FILE")
98
+ _apply_config_file_override("--file-extractors-config", "FILE_EXTRACTORS_CONFIG_FILE")
99
+
100
+ _env_file_path, _env_file_is_custom = _get_env_file_from_args()
101
+ if not _env_file_path.exists():
102
+ if _env_file_is_custom:
103
+ print(f"Error: specified env file not found: {_env_file_path}", file=sys.stderr)
104
+ sys.exit(2)
105
+ else:
106
+ print(f"Warning: default env file not found: {_env_file_path}", file=sys.stderr)
107
+
108
+ # Phase 1b: Apply config override flags before loading the env file.
109
+ _apply_config_overrides_from_args()
110
+ load_dotenv(dotenv_path=str(_env_file_path))
111
+
112
+ # Now safe to import atlas code (which transitively imports litellm)
113
+ from atlas.atlas_client import AtlasClient # noqa: E402
114
+
115
+ # Belt-and-suspenders: also quiet the Python loggers litellm creates
116
+ for _name in ("LiteLLM", "LiteLLM Proxy", "LiteLLM Router", "litellm", "httpx"):
117
+ logging.getLogger(_name).setLevel(logging.WARNING)
118
+
119
+
120
+ def build_parser() -> argparse.ArgumentParser:
121
+ parser = argparse.ArgumentParser(
122
+ prog="atlas-chat",
123
+ description="Non-interactive CLI for Atlas LLM chat with MCP tools and RAG.",
124
+ )
125
+ parser.add_argument(
126
+ "prompt",
127
+ nargs="?",
128
+ default=None,
129
+ help="Chat prompt text, or '-' to read from stdin.",
130
+ )
131
+ parser.add_argument("--model", default=None, help="LLM model name (uses config default if omitted).")
132
+ parser.add_argument("--tools", default=None, help="Comma-separated list of tool names to enable.")
133
+ parser.add_argument("-o", "--output", default=None, help="Write final response to file path.")
134
+ parser.add_argument("--json", dest="json_output", action="store_true", help="Output structured JSON.")
135
+ parser.add_argument("--user-email", default=None, help="Override user identity.")
136
+ parser.add_argument("--list-tools", action="store_true", help="Print available tools and exit.")
137
+ parser.add_argument(
138
+ "--data-sources",
139
+ default=None,
140
+ help="Comma-separated list of RAG data source names to query.",
141
+ )
142
+ parser.add_argument(
143
+ "--only-rag",
144
+ action="store_true",
145
+ help="Use only RAG without tools (RAG-only mode).",
146
+ )
147
+ parser.add_argument(
148
+ "--list-data-sources",
149
+ action="store_true",
150
+ help="Print available RAG data sources and exit.",
151
+ )
152
+ parser.add_argument(
153
+ "--env-file",
154
+ default=None,
155
+ help="Path to custom .env file (default: project root .env). Parsed early before other imports.",
156
+ )
157
+
158
+ # Config override flags (useful for testing and CI)
159
+ parser.add_argument(
160
+ "--config-overrides",
161
+ default=None,
162
+ help="Override config overrides directory (sets APP_CONFIG_OVERRIDES).",
163
+ )
164
+ parser.add_argument(
165
+ "--config-defaults",
166
+ default=None,
167
+ help="Override config defaults directory (sets APP_CONFIG_DEFAULTS).",
168
+ )
169
+ parser.add_argument(
170
+ "--llm-config",
171
+ default=None,
172
+ help="Override LLM config file (sets LLM_CONFIG_FILE). Accepts a filename or path.",
173
+ )
174
+ parser.add_argument(
175
+ "--mcp-config",
176
+ default=None,
177
+ help="Override MCP config file (sets MCP_CONFIG_FILE). Accepts a filename or path.",
178
+ )
179
+ parser.add_argument(
180
+ "--rag-sources-config",
181
+ default=None,
182
+ help="Override RAG sources config file (sets RAG_SOURCES_CONFIG_FILE). Accepts a filename or path.",
183
+ )
184
+ parser.add_argument(
185
+ "--help-config",
186
+ default=None,
187
+ help="Override help config file (sets HELP_CONFIG_FILE). Accepts a filename or path.",
188
+ )
189
+ parser.add_argument(
190
+ "--messages-config",
191
+ default=None,
192
+ help="Override messages config file (sets MESSAGES_CONFIG_FILE). Accepts a filename or path.",
193
+ )
194
+ parser.add_argument(
195
+ "--tool-approvals-config",
196
+ default=None,
197
+ help="Override tool approvals config file (sets TOOL_APPROVALS_CONFIG_FILE). Accepts a filename or path.",
198
+ )
199
+ parser.add_argument(
200
+ "--splash-config",
201
+ default=None,
202
+ help="Override splash config file (sets SPLASH_CONFIG_FILE). Accepts a filename or path.",
203
+ )
204
+ parser.add_argument(
205
+ "--file-extractors-config",
206
+ default=None,
207
+ help="Override file extractors config file (sets FILE_EXTRACTORS_CONFIG_FILE). Accepts a filename or path.",
208
+ )
209
+ return parser
210
+
211
+
212
+ async def list_tools(*, json_output: bool = False) -> int:
213
+ """Discover and print all available tools in CLI-usable format."""
214
+ client = AtlasClient()
215
+ try:
216
+ await client.initialize()
217
+ mcp_manager = client._factory.get_mcp_manager()
218
+ tool_index = getattr(mcp_manager, "_tool_index", {})
219
+ if not tool_index:
220
+ if json_output:
221
+ print(json.dumps({"servers": {}, "tools": []}, indent=2))
222
+ return 0
223
+ print("No tools discovered.", file=sys.stderr)
224
+ return 1
225
+ # Group by server
226
+ servers: dict[str, list[str]] = {}
227
+ for full_name, info in sorted(tool_index.items()):
228
+ server = info["server"]
229
+ servers.setdefault(server, []).append(full_name)
230
+ if json_output:
231
+ tools = [name for names in servers.values() for name in names]
232
+ print(json.dumps({"servers": servers, "tools": tools}, indent=2))
233
+ return 0
234
+ for server, tools in servers.items():
235
+ print(f"{server}:")
236
+ for name in tools:
237
+ print(f" {name}")
238
+ return 0
239
+ finally:
240
+ await client.cleanup()
241
+
242
+
243
+ async def list_data_sources(user_email: str = None, *, json_output: bool = False) -> int:
244
+ """Discover and print all available RAG data sources."""
245
+ client = AtlasClient()
246
+ try:
247
+ result = await client.list_data_sources(user_email=user_email)
248
+ if json_output:
249
+ print(json.dumps(result, indent=2))
250
+ return 0
251
+ servers = result.get("servers", {})
252
+ discovered = result.get("sources", [])
253
+
254
+ if not servers and not discovered:
255
+ print("No RAG data sources configured.", file=sys.stderr)
256
+ return 1
257
+
258
+ # Show configured servers
259
+ if servers:
260
+ print("Configured RAG servers:")
261
+ for name, info in sorted(servers.items()):
262
+ display_name = info.get("display_name", name)
263
+ source_type = info.get("type", "unknown")
264
+ desc = info.get("description", "")
265
+ print(f" {display_name} ({source_type})")
266
+ if desc:
267
+ print(f" {desc}")
268
+ print()
269
+
270
+ # Show discovered sources (these are the actual --data-sources values)
271
+ if discovered:
272
+ print("Available data sources (use with --data-sources):")
273
+ for source_id in discovered:
274
+ print(f" {source_id}")
275
+ else:
276
+ print("No data sources discovered. Servers may not expose rag_discover_resources.")
277
+ print("For MCP RAG servers, try: --data-sources SERVER_NAME:SOURCE_ID")
278
+
279
+ return 0
280
+ finally:
281
+ await client.cleanup()
282
+
283
+
284
+ async def run(args: argparse.Namespace) -> int:
285
+ if args.list_tools:
286
+ return await list_tools(json_output=args.json_output)
287
+
288
+ if args.list_data_sources:
289
+ return await list_data_sources(user_email=args.user_email, json_output=args.json_output)
290
+
291
+ # Resolve prompt
292
+ prompt = args.prompt
293
+ if prompt == "-" or (prompt is None and not sys.stdin.isatty()):
294
+ prompt = sys.stdin.read().strip()
295
+ if not prompt:
296
+ print("Error: no prompt provided.", file=sys.stderr)
297
+ return 2
298
+
299
+ selected_tools = None
300
+ if args.tools:
301
+ selected_tools = [t.strip() for t in args.tools.split(",") if t.strip()]
302
+
303
+ selected_data_sources = None
304
+ if args.data_sources:
305
+ selected_data_sources = [s.strip() for s in args.data_sources.split(",") if s.strip()]
306
+
307
+ # In JSON or output-file mode, collect rather than stream
308
+ streaming = not args.json_output and args.output is None
309
+
310
+ client = AtlasClient()
311
+ try:
312
+ result = await client.chat(
313
+ prompt=prompt,
314
+ model=args.model,
315
+ agent_mode=False,
316
+ selected_tools=selected_tools,
317
+ selected_data_sources=selected_data_sources,
318
+ only_rag=args.only_rag,
319
+ user_email=args.user_email,
320
+ session_id=None,
321
+ streaming=streaming,
322
+ )
323
+
324
+ if args.json_output:
325
+ print(json.dumps(result.to_dict(), indent=2))
326
+ elif args.output:
327
+ Path(args.output).write_text(result.message, encoding="utf-8")
328
+ print(f"Output written to {args.output}", file=sys.stderr)
329
+ # If streaming, output was already printed live
330
+
331
+ return 0
332
+ except Exception as exc:
333
+ print(f"Error: {exc}", file=sys.stderr)
334
+ logging.getLogger(__name__).debug("CLI error details", exc_info=True)
335
+ return 1
336
+ finally:
337
+ await client.cleanup()
338
+
339
+
340
+ def main() -> None:
341
+ parser = build_parser()
342
+ args = parser.parse_args()
343
+ sys.exit(asyncio.run(run(args)))
344
+
345
+
346
+ if __name__ == "__main__":
347
+ main()
atlas/atlas_client.py ADDED
@@ -0,0 +1,238 @@
1
+ """Python client API for Atlas chat -- headless, non-interactive usage."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Dict, List, Optional
7
+ from uuid import UUID, uuid4
8
+
9
+ from atlas.infrastructure.events.cli_event_publisher import CLIEventPublisher
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ @dataclass
15
+ class ChatResult:
16
+ """Structured result from a chat call."""
17
+
18
+ message: str
19
+ tool_calls: List[Dict[str, Any]] = field(default_factory=list)
20
+ files: Dict[str, Any] = field(default_factory=dict)
21
+ canvas_content: Optional[str] = None
22
+ session_id: Optional[UUID] = None
23
+
24
+ def to_dict(self) -> Dict[str, Any]:
25
+ return {
26
+ "message": self.message,
27
+ "tool_calls": self.tool_calls,
28
+ "files": self.files,
29
+ "canvas_content": self.canvas_content,
30
+ "session_id": str(self.session_id) if self.session_id else None,
31
+ }
32
+
33
+
34
+ class AtlasClient:
35
+ """
36
+ Headless Python client for Atlas chat.
37
+
38
+ Wraps AppFactory + ChatService for programmatic one-shot or
39
+ multi-turn LLM conversations with MCP tools, RAG, and agent mode.
40
+ """
41
+
42
+ def __init__(self) -> None:
43
+ self._factory = None
44
+ self._initialized = False
45
+
46
+ async def initialize(self) -> None:
47
+ """Initialize the backend (MCP discovery, etc.)."""
48
+ if self._initialized:
49
+ return
50
+
51
+ from atlas.infrastructure.app_factory import AppFactory
52
+
53
+ self._factory = AppFactory()
54
+
55
+ mcp_manager = self._factory.get_mcp_manager()
56
+ try:
57
+ await mcp_manager.initialize_clients()
58
+ await mcp_manager.discover_tools()
59
+ await mcp_manager.discover_prompts()
60
+ except Exception:
61
+ logger.warning("MCP initialization failed; continuing without tools")
62
+
63
+ # LiteLLMCaller sets litellm.set_verbose = debug_mode, which causes
64
+ # litellm to print() debug info to stdout. Force it off for CLI use.
65
+ import litellm as _litellm
66
+ _litellm.set_verbose = False
67
+ _litellm.suppress_debug_info = True
68
+
69
+ self._initialized = True
70
+
71
+ async def chat(
72
+ self,
73
+ prompt: str,
74
+ *,
75
+ model: Optional[str] = None,
76
+ agent_mode: bool = False,
77
+ selected_tools: Optional[List[str]] = None,
78
+ selected_data_sources: Optional[List[str]] = None,
79
+ only_rag: bool = False,
80
+ user_email: Optional[str] = None,
81
+ session_id: Optional[UUID] = None,
82
+ max_steps: int = 10,
83
+ temperature: float = 0.7,
84
+ streaming: bool = False,
85
+ quiet: bool = False,
86
+ ) -> ChatResult:
87
+ """
88
+ Send a chat message and return the result.
89
+
90
+ Args:
91
+ prompt: User message text.
92
+ model: LLM model name. Uses config default if not specified.
93
+ agent_mode: Enable agent loop for multi-step tool use.
94
+ selected_tools: List of tool names to enable.
95
+ selected_data_sources: List of RAG data source names to query.
96
+ only_rag: If True, use only RAG without tools (RAG-only mode).
97
+ user_email: User identity for auth-filtered tools/RAG.
98
+ session_id: Reuse an existing session for multi-turn.
99
+ max_steps: Max agent iterations.
100
+ temperature: LLM temperature.
101
+ streaming: If True, stream tokens to stdout as they arrive.
102
+ quiet: Suppress status output on stderr (only affects streaming mode).
103
+
104
+ Returns:
105
+ ChatResult with assistant message, tool calls, files, etc.
106
+ """
107
+ await self.initialize()
108
+
109
+ if session_id is None:
110
+ session_id = uuid4()
111
+
112
+ if model is None:
113
+ models = self._factory.get_config_manager().llm_config.models
114
+ if models:
115
+ # models is a dict of {display_name: ModelConfig}
116
+ first_key = next(iter(models))
117
+ model = first_key
118
+ else:
119
+ model = "gpt-4o"
120
+
121
+ if user_email is None:
122
+ cfg = self._factory.get_config_manager()
123
+ user_email = cfg.app_settings.test_user or "cli@atlas.local"
124
+
125
+ event_publisher = CLIEventPublisher(streaming=streaming, quiet=quiet)
126
+ chat_service = self._factory.create_chat_service(connection=None)
127
+ # Replace the default event publisher with our CLI one
128
+ chat_service.event_publisher = event_publisher
129
+ # Re-initialize mode runners with the new publisher
130
+ chat_service.plain_mode.event_publisher = event_publisher
131
+ chat_service.rag_mode.event_publisher = event_publisher
132
+ chat_service.tools_mode.event_publisher = event_publisher
133
+ chat_service.tools_mode.skip_approval = True
134
+ chat_service.agent_mode.event_publisher = event_publisher
135
+ chat_service.agent_mode.agent_loop_factory.skip_approval = True
136
+
137
+ await chat_service.handle_chat_message(
138
+ session_id=session_id,
139
+ content=prompt,
140
+ model=model,
141
+ selected_tools=selected_tools,
142
+ selected_data_sources=selected_data_sources,
143
+ only_rag=only_rag,
144
+ agent_mode=agent_mode,
145
+ agent_max_steps=max_steps,
146
+ user_email=user_email,
147
+ temperature=temperature,
148
+ )
149
+
150
+ collected = event_publisher.get_result()
151
+ return ChatResult(
152
+ message=collected.message,
153
+ tool_calls=collected.tool_calls,
154
+ files=collected.files,
155
+ canvas_content=collected.canvas_content,
156
+ session_id=session_id,
157
+ )
158
+
159
+ def chat_sync(self, prompt: str, **kwargs) -> ChatResult:
160
+ """Synchronous wrapper around chat()."""
161
+ return asyncio.run(self.chat(prompt, **kwargs))
162
+
163
+ async def list_data_sources(self, user_email: Optional[str] = None) -> Dict[str, Any]:
164
+ """Discover and list available RAG data sources.
165
+
166
+ Calls the RAG discovery mechanism to get actual available sources
167
+ with their qualified IDs (format: server:source_id).
168
+
169
+ Args:
170
+ user_email: User identity for auth-filtered sources.
171
+
172
+ Returns:
173
+ Dict with 'servers' (config info) and 'sources' (discovered qualified IDs).
174
+ """
175
+ await self.initialize()
176
+ cfg = self._factory.get_config_manager()
177
+
178
+ # Return empty results when RAG feature is disabled
179
+ if not cfg.app_settings.feature_rag_enabled:
180
+ logger.info("RAG discovery skipped (FEATURE_RAG_ENABLED=false)")
181
+ return {"servers": {}, "sources": []}
182
+
183
+ if user_email is None:
184
+ user_email = cfg.app_settings.test_user or "cli@atlas.local"
185
+
186
+ # Get server config info
187
+ servers = {}
188
+ for name, source in cfg.rag_sources_config.sources.items():
189
+ if source.enabled:
190
+ servers[name] = {
191
+ "type": source.type,
192
+ "display_name": source.display_name or name,
193
+ "description": source.description,
194
+ }
195
+
196
+ discovered_sources: List[str] = []
197
+ rag_service = self._factory.get_unified_rag_service()
198
+
199
+ # Best-effort discovery across HTTP sources
200
+ if rag_service:
201
+ try:
202
+ rag_servers = await rag_service.discover_data_sources(username=user_email)
203
+ for server in rag_servers:
204
+ server_name = server.get("server")
205
+ for src in server.get("sources", []) or []:
206
+ source_id = src.get("id")
207
+ if server_name and source_id:
208
+ discovered_sources.append(f"{server_name}:{source_id}")
209
+ except Exception as e:
210
+ logger.warning("HTTP RAG discovery failed: %s", e)
211
+
212
+ # Best-effort discovery across MCP RAG sources
213
+ if rag_service and getattr(rag_service, "rag_mcp_service", None):
214
+ try:
215
+ mcp_sources = await rag_service.rag_mcp_service.discover_data_sources(user_email)
216
+ if mcp_sources:
217
+ discovered_sources.extend(mcp_sources)
218
+ except Exception as e:
219
+ logger.warning("MCP RAG discovery failed: %s", e)
220
+
221
+ # Deduplicate while preserving order
222
+ seen = set()
223
+ deduped: List[str] = []
224
+ for s in discovered_sources:
225
+ if s not in seen:
226
+ seen.add(s)
227
+ deduped.append(s)
228
+
229
+ return {
230
+ "servers": servers,
231
+ "sources": deduped,
232
+ }
233
+
234
+ async def cleanup(self) -> None:
235
+ """Cleanup MCP connections."""
236
+ if self._factory:
237
+ mcp = self._factory.get_mcp_manager()
238
+ await mcp.cleanup()
atlas/core/__init__.py ADDED
File without changes