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