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.
- atlas/__init__.py +40 -0
- atlas/application/__init__.py +7 -0
- atlas/application/chat/__init__.py +7 -0
- atlas/application/chat/agent/__init__.py +10 -0
- atlas/application/chat/agent/act_loop.py +179 -0
- atlas/application/chat/agent/factory.py +142 -0
- atlas/application/chat/agent/protocols.py +46 -0
- atlas/application/chat/agent/react_loop.py +338 -0
- atlas/application/chat/agent/think_act_loop.py +171 -0
- atlas/application/chat/approval_manager.py +151 -0
- atlas/application/chat/elicitation_manager.py +191 -0
- atlas/application/chat/events/__init__.py +1 -0
- atlas/application/chat/events/agent_event_relay.py +112 -0
- atlas/application/chat/modes/__init__.py +1 -0
- atlas/application/chat/modes/agent.py +125 -0
- atlas/application/chat/modes/plain.py +74 -0
- atlas/application/chat/modes/rag.py +81 -0
- atlas/application/chat/modes/tools.py +179 -0
- atlas/application/chat/orchestrator.py +213 -0
- atlas/application/chat/policies/__init__.py +1 -0
- atlas/application/chat/policies/tool_authorization.py +99 -0
- atlas/application/chat/preprocessors/__init__.py +1 -0
- atlas/application/chat/preprocessors/message_builder.py +92 -0
- atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
- atlas/application/chat/service.py +454 -0
- atlas/application/chat/utilities/__init__.py +6 -0
- atlas/application/chat/utilities/error_handler.py +367 -0
- atlas/application/chat/utilities/event_notifier.py +546 -0
- atlas/application/chat/utilities/file_processor.py +613 -0
- atlas/application/chat/utilities/tool_executor.py +789 -0
- atlas/atlas_chat_cli.py +347 -0
- atlas/atlas_client.py +238 -0
- atlas/core/__init__.py +0 -0
- atlas/core/auth.py +205 -0
- atlas/core/authorization_manager.py +27 -0
- atlas/core/capabilities.py +123 -0
- atlas/core/compliance.py +215 -0
- atlas/core/domain_whitelist.py +147 -0
- atlas/core/domain_whitelist_middleware.py +82 -0
- atlas/core/http_client.py +28 -0
- atlas/core/log_sanitizer.py +102 -0
- atlas/core/metrics_logger.py +59 -0
- atlas/core/middleware.py +131 -0
- atlas/core/otel_config.py +242 -0
- atlas/core/prompt_risk.py +200 -0
- atlas/core/rate_limit.py +0 -0
- atlas/core/rate_limit_middleware.py +64 -0
- atlas/core/security_headers_middleware.py +51 -0
- atlas/domain/__init__.py +37 -0
- atlas/domain/chat/__init__.py +1 -0
- atlas/domain/chat/dtos.py +85 -0
- atlas/domain/errors.py +96 -0
- atlas/domain/messages/__init__.py +12 -0
- atlas/domain/messages/models.py +160 -0
- atlas/domain/rag_mcp_service.py +664 -0
- atlas/domain/sessions/__init__.py +7 -0
- atlas/domain/sessions/models.py +36 -0
- atlas/domain/unified_rag_service.py +371 -0
- atlas/infrastructure/__init__.py +10 -0
- atlas/infrastructure/app_factory.py +135 -0
- atlas/infrastructure/events/__init__.py +1 -0
- atlas/infrastructure/events/cli_event_publisher.py +140 -0
- atlas/infrastructure/events/websocket_publisher.py +140 -0
- atlas/infrastructure/sessions/in_memory_repository.py +56 -0
- atlas/infrastructure/transport/__init__.py +7 -0
- atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
- atlas/init_cli.py +226 -0
- atlas/interfaces/__init__.py +15 -0
- atlas/interfaces/events.py +134 -0
- atlas/interfaces/llm.py +54 -0
- atlas/interfaces/rag.py +40 -0
- atlas/interfaces/sessions.py +75 -0
- atlas/interfaces/tools.py +57 -0
- atlas/interfaces/transport.py +24 -0
- atlas/main.py +564 -0
- atlas/mcp/api_key_demo/README.md +76 -0
- atlas/mcp/api_key_demo/main.py +172 -0
- atlas/mcp/api_key_demo/run.sh +56 -0
- atlas/mcp/basictable/main.py +147 -0
- atlas/mcp/calculator/main.py +149 -0
- atlas/mcp/code-executor/execution_engine.py +98 -0
- atlas/mcp/code-executor/execution_environment.py +95 -0
- atlas/mcp/code-executor/main.py +528 -0
- atlas/mcp/code-executor/result_processing.py +276 -0
- atlas/mcp/code-executor/script_generation.py +195 -0
- atlas/mcp/code-executor/security_checker.py +140 -0
- atlas/mcp/corporate_cars/main.py +437 -0
- atlas/mcp/csv_reporter/main.py +545 -0
- atlas/mcp/duckduckgo/main.py +182 -0
- atlas/mcp/elicitation_demo/README.md +171 -0
- atlas/mcp/elicitation_demo/main.py +262 -0
- atlas/mcp/env-demo/README.md +158 -0
- atlas/mcp/env-demo/main.py +199 -0
- atlas/mcp/file_size_test/main.py +284 -0
- atlas/mcp/filesystem/main.py +348 -0
- atlas/mcp/image_demo/main.py +113 -0
- atlas/mcp/image_demo/requirements.txt +4 -0
- atlas/mcp/logging_demo/README.md +72 -0
- atlas/mcp/logging_demo/main.py +103 -0
- atlas/mcp/many_tools_demo/main.py +50 -0
- atlas/mcp/order_database/__init__.py +0 -0
- atlas/mcp/order_database/main.py +369 -0
- atlas/mcp/order_database/signal_data.csv +1001 -0
- atlas/mcp/pdfbasic/main.py +394 -0
- atlas/mcp/pptx_generator/main.py +760 -0
- atlas/mcp/pptx_generator/requirements.txt +13 -0
- atlas/mcp/pptx_generator/run_test.sh +1 -0
- atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
- atlas/mcp/progress_demo/main.py +167 -0
- atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
- atlas/mcp/progress_updates_demo/README.md +120 -0
- atlas/mcp/progress_updates_demo/main.py +497 -0
- atlas/mcp/prompts/main.py +222 -0
- atlas/mcp/public_demo/main.py +189 -0
- atlas/mcp/sampling_demo/README.md +169 -0
- atlas/mcp/sampling_demo/main.py +234 -0
- atlas/mcp/thinking/main.py +77 -0
- atlas/mcp/tool_planner/main.py +240 -0
- atlas/mcp/ui-demo/badmesh.png +0 -0
- atlas/mcp/ui-demo/main.py +383 -0
- atlas/mcp/ui-demo/templates/button_demo.html +32 -0
- atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
- atlas/mcp/ui-demo/templates/form_demo.html +28 -0
- atlas/mcp/username-override-demo/README.md +320 -0
- atlas/mcp/username-override-demo/main.py +308 -0
- atlas/modules/__init__.py +0 -0
- atlas/modules/config/__init__.py +34 -0
- atlas/modules/config/cli.py +231 -0
- atlas/modules/config/config_manager.py +1096 -0
- atlas/modules/file_storage/__init__.py +22 -0
- atlas/modules/file_storage/cli.py +330 -0
- atlas/modules/file_storage/content_extractor.py +290 -0
- atlas/modules/file_storage/manager.py +295 -0
- atlas/modules/file_storage/mock_s3_client.py +402 -0
- atlas/modules/file_storage/s3_client.py +417 -0
- atlas/modules/llm/__init__.py +19 -0
- atlas/modules/llm/caller.py +287 -0
- atlas/modules/llm/litellm_caller.py +675 -0
- atlas/modules/llm/models.py +19 -0
- atlas/modules/mcp_tools/__init__.py +17 -0
- atlas/modules/mcp_tools/client.py +2123 -0
- atlas/modules/mcp_tools/token_storage.py +556 -0
- atlas/modules/prompts/prompt_provider.py +130 -0
- atlas/modules/rag/__init__.py +24 -0
- atlas/modules/rag/atlas_rag_client.py +336 -0
- atlas/modules/rag/client.py +129 -0
- atlas/routes/admin_routes.py +865 -0
- atlas/routes/config_routes.py +484 -0
- atlas/routes/feedback_routes.py +361 -0
- atlas/routes/files_routes.py +274 -0
- atlas/routes/health_routes.py +40 -0
- atlas/routes/mcp_auth_routes.py +223 -0
- atlas/server_cli.py +164 -0
- atlas/tests/conftest.py +20 -0
- atlas/tests/integration/test_mcp_auth_integration.py +152 -0
- atlas/tests/manual_test_sampling.py +87 -0
- atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
- atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
- atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
- atlas/tests/test_agent_roa.py +135 -0
- atlas/tests/test_app_factory_smoke.py +47 -0
- atlas/tests/test_approval_manager.py +439 -0
- atlas/tests/test_atlas_client.py +188 -0
- atlas/tests/test_atlas_rag_client.py +447 -0
- atlas/tests/test_atlas_rag_integration.py +224 -0
- atlas/tests/test_attach_file_flow.py +287 -0
- atlas/tests/test_auth_utils.py +165 -0
- atlas/tests/test_backend_public_url.py +185 -0
- atlas/tests/test_banner_logging.py +287 -0
- atlas/tests/test_capability_tokens_and_injection.py +203 -0
- atlas/tests/test_compliance_level.py +54 -0
- atlas/tests/test_compliance_manager.py +253 -0
- atlas/tests/test_config_manager.py +617 -0
- atlas/tests/test_config_manager_paths.py +12 -0
- atlas/tests/test_core_auth.py +18 -0
- atlas/tests/test_core_utils.py +190 -0
- atlas/tests/test_docker_env_sync.py +202 -0
- atlas/tests/test_domain_errors.py +329 -0
- atlas/tests/test_domain_whitelist.py +359 -0
- atlas/tests/test_elicitation_manager.py +408 -0
- atlas/tests/test_elicitation_routing.py +296 -0
- atlas/tests/test_env_demo_server.py +88 -0
- atlas/tests/test_error_classification.py +113 -0
- atlas/tests/test_error_flow_integration.py +116 -0
- atlas/tests/test_feedback_routes.py +333 -0
- atlas/tests/test_file_content_extraction.py +1134 -0
- atlas/tests/test_file_extraction_routes.py +158 -0
- atlas/tests/test_file_library.py +107 -0
- atlas/tests/test_file_manager_unit.py +18 -0
- atlas/tests/test_health_route.py +49 -0
- atlas/tests/test_http_client_stub.py +8 -0
- atlas/tests/test_imports_smoke.py +30 -0
- atlas/tests/test_interfaces_llm_response.py +9 -0
- atlas/tests/test_issue_access_denied_fix.py +136 -0
- atlas/tests/test_llm_env_expansion.py +836 -0
- atlas/tests/test_log_level_sensitive_data.py +285 -0
- atlas/tests/test_mcp_auth_routes.py +341 -0
- atlas/tests/test_mcp_client_auth.py +331 -0
- atlas/tests/test_mcp_data_injection.py +270 -0
- atlas/tests/test_mcp_get_authorized_servers.py +95 -0
- atlas/tests/test_mcp_hot_reload.py +512 -0
- atlas/tests/test_mcp_image_content.py +424 -0
- atlas/tests/test_mcp_logging.py +172 -0
- atlas/tests/test_mcp_progress_updates.py +313 -0
- atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
- atlas/tests/test_mcp_prompts_server.py +39 -0
- atlas/tests/test_mcp_tool_result_parsing.py +296 -0
- atlas/tests/test_metrics_logger.py +56 -0
- atlas/tests/test_middleware_auth.py +379 -0
- atlas/tests/test_prompt_risk_and_acl.py +141 -0
- atlas/tests/test_rag_mcp_aggregator.py +204 -0
- atlas/tests/test_rag_mcp_service.py +224 -0
- atlas/tests/test_rate_limit_middleware.py +45 -0
- atlas/tests/test_routes_config_smoke.py +60 -0
- atlas/tests/test_routes_files_download_token.py +41 -0
- atlas/tests/test_routes_files_health.py +18 -0
- atlas/tests/test_runtime_imports.py +53 -0
- atlas/tests/test_sampling_integration.py +482 -0
- atlas/tests/test_security_admin_routes.py +61 -0
- atlas/tests/test_security_capability_tokens.py +65 -0
- atlas/tests/test_security_file_stats_scope.py +21 -0
- atlas/tests/test_security_header_injection.py +191 -0
- atlas/tests/test_security_headers_and_filename.py +63 -0
- atlas/tests/test_shared_session_repository.py +101 -0
- atlas/tests/test_system_prompt_loading.py +181 -0
- atlas/tests/test_token_storage.py +505 -0
- atlas/tests/test_tool_approval_config.py +93 -0
- atlas/tests/test_tool_approval_utils.py +356 -0
- atlas/tests/test_tool_authorization_group_filtering.py +223 -0
- atlas/tests/test_tool_details_in_config.py +108 -0
- atlas/tests/test_tool_planner.py +300 -0
- atlas/tests/test_unified_rag_service.py +398 -0
- atlas/tests/test_username_override_in_approval.py +258 -0
- atlas/tests/test_websocket_auth_header.py +168 -0
- atlas/version.py +6 -0
- atlas_chat-0.1.0.data/data/.env.example +253 -0
- atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
- atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
- atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
- atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
- atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
- atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
- atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
- atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
- atlas_chat-0.1.0.dist-info/METADATA +236 -0
- atlas_chat-0.1.0.dist-info/RECORD +250 -0
- atlas_chat-0.1.0.dist-info/WHEEL +5 -0
- atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
- atlas_chat-0.1.0.dist-info/top_level.txt +1 -0
atlas/atlas_chat_cli.py
ADDED
|
@@ -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
|