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
|
@@ -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,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
|
+
]
|