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,454 @@
|
|
|
1
|
+
"""Chat service - core business logic for chat operations."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from atlas.core.log_sanitizer import sanitize_for_logging
|
|
8
|
+
from atlas.domain.errors import DomainError
|
|
9
|
+
from atlas.domain.messages.models import MessageType, ToolResult
|
|
10
|
+
from atlas.domain.sessions.models import Session
|
|
11
|
+
from atlas.interfaces.events import EventPublisher
|
|
12
|
+
from atlas.interfaces.llm import LLMProtocol
|
|
13
|
+
from atlas.interfaces.sessions import SessionRepository
|
|
14
|
+
from atlas.interfaces.tools import ToolManagerProtocol
|
|
15
|
+
from atlas.interfaces.transport import ChatConnectionProtocol
|
|
16
|
+
from atlas.modules.config import ConfigManager
|
|
17
|
+
from atlas.modules.prompts.prompt_provider import PromptProvider
|
|
18
|
+
|
|
19
|
+
from .agent import AgentLoopFactory
|
|
20
|
+
from .modes.agent import AgentModeRunner
|
|
21
|
+
from .modes.plain import PlainModeRunner
|
|
22
|
+
from .modes.rag import RagModeRunner
|
|
23
|
+
from .modes.tools import ToolsModeRunner
|
|
24
|
+
|
|
25
|
+
# Import new refactored modules
|
|
26
|
+
from .policies.tool_authorization import ToolAuthorizationService
|
|
27
|
+
from .preprocessors.message_builder import MessageBuilder, build_session_context
|
|
28
|
+
from .preprocessors.prompt_override_service import PromptOverrideService
|
|
29
|
+
|
|
30
|
+
# Import utilities
|
|
31
|
+
from .utilities import error_handler, file_processor
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# Type hint for the update callback
|
|
36
|
+
UpdateCallback = Callable[[Dict[str, Any]], Awaitable[None]]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ChatService:
|
|
40
|
+
"""
|
|
41
|
+
Core chat service that orchestrates chat operations.
|
|
42
|
+
Transport-agnostic, testable business logic.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
llm: LLMProtocol,
|
|
48
|
+
tool_manager: Optional[ToolManagerProtocol] = None,
|
|
49
|
+
connection: Optional[ChatConnectionProtocol] = None,
|
|
50
|
+
config_manager: Optional[ConfigManager] = None,
|
|
51
|
+
file_manager: Optional[Any] = None,
|
|
52
|
+
agent_loop_factory: Optional[AgentLoopFactory] = None,
|
|
53
|
+
event_publisher: Optional[EventPublisher] = None,
|
|
54
|
+
session_repository: Optional[SessionRepository] = None,
|
|
55
|
+
):
|
|
56
|
+
"""
|
|
57
|
+
Initialize chat service with dependencies.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
llm: LLM protocol implementation
|
|
61
|
+
tool_manager: Optional tool manager
|
|
62
|
+
connection: Optional connection for sending updates
|
|
63
|
+
config_manager: Configuration manager
|
|
64
|
+
file_manager: File manager for S3 operations
|
|
65
|
+
agent_loop_factory: Factory for creating agent loops (optional)
|
|
66
|
+
event_publisher: Event publisher for UI updates (optional, will create default)
|
|
67
|
+
session_repository: Session storage repository (optional, will create default)
|
|
68
|
+
"""
|
|
69
|
+
self.llm = llm
|
|
70
|
+
self.tool_manager = tool_manager
|
|
71
|
+
self.connection = connection
|
|
72
|
+
self.config_manager = config_manager
|
|
73
|
+
self.prompt_provider: Optional[PromptProvider] = (
|
|
74
|
+
PromptProvider(self.config_manager) if self.config_manager else None
|
|
75
|
+
)
|
|
76
|
+
self.file_manager = file_manager
|
|
77
|
+
|
|
78
|
+
# Initialize or use provided event publisher
|
|
79
|
+
if event_publisher is not None:
|
|
80
|
+
self.event_publisher = event_publisher
|
|
81
|
+
else:
|
|
82
|
+
# Create default WebSocket publisher
|
|
83
|
+
from atlas.infrastructure.events.websocket_publisher import WebSocketEventPublisher
|
|
84
|
+
self.event_publisher = WebSocketEventPublisher(connection=self.connection)
|
|
85
|
+
|
|
86
|
+
# Initialize or use provided session repository
|
|
87
|
+
if session_repository is not None:
|
|
88
|
+
self.session_repository = session_repository
|
|
89
|
+
else:
|
|
90
|
+
# Create default in-memory repository
|
|
91
|
+
from atlas.infrastructure.sessions.in_memory_repository import InMemorySessionRepository
|
|
92
|
+
self.session_repository = InMemorySessionRepository()
|
|
93
|
+
|
|
94
|
+
# Legacy sessions dict - deprecated, use session_repository instead
|
|
95
|
+
# Kept temporarily for backward compatibility
|
|
96
|
+
self.sessions: Dict[UUID, Session] = {}
|
|
97
|
+
|
|
98
|
+
# Initialize refactored services
|
|
99
|
+
self.tool_authorization = ToolAuthorizationService(tool_manager=self.tool_manager)
|
|
100
|
+
self.prompt_override = PromptOverrideService(tool_manager=self.tool_manager)
|
|
101
|
+
self.message_builder = MessageBuilder()
|
|
102
|
+
|
|
103
|
+
# Initialize mode runners
|
|
104
|
+
self.plain_mode = PlainModeRunner(
|
|
105
|
+
llm=self.llm,
|
|
106
|
+
event_publisher=self.event_publisher,
|
|
107
|
+
)
|
|
108
|
+
self.rag_mode = RagModeRunner(
|
|
109
|
+
llm=self.llm,
|
|
110
|
+
event_publisher=self.event_publisher,
|
|
111
|
+
)
|
|
112
|
+
self.tools_mode = ToolsModeRunner(
|
|
113
|
+
llm=self.llm,
|
|
114
|
+
tool_manager=self.tool_manager,
|
|
115
|
+
event_publisher=self.event_publisher,
|
|
116
|
+
prompt_provider=self.prompt_provider,
|
|
117
|
+
artifact_processor=self._update_session_from_tool_results,
|
|
118
|
+
config_manager=self.config_manager,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# Agent loop factory - create if not provided
|
|
124
|
+
if agent_loop_factory is not None:
|
|
125
|
+
self.agent_loop_factory = agent_loop_factory
|
|
126
|
+
else:
|
|
127
|
+
self.agent_loop_factory = AgentLoopFactory(
|
|
128
|
+
llm=self.llm,
|
|
129
|
+
tool_manager=self.tool_manager,
|
|
130
|
+
prompt_provider=self.prompt_provider,
|
|
131
|
+
connection=self.connection,
|
|
132
|
+
config_manager=self.config_manager,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Get default strategy from config
|
|
136
|
+
self.default_agent_strategy = "think-act"
|
|
137
|
+
try:
|
|
138
|
+
if self.config_manager:
|
|
139
|
+
config_strategy = self.config_manager.app_settings.agent_loop_strategy
|
|
140
|
+
if config_strategy:
|
|
141
|
+
self.default_agent_strategy = config_strategy.lower()
|
|
142
|
+
except Exception:
|
|
143
|
+
# Ignore config errors - fall back to default strategy
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
# Initialize agent mode runner (after agent_loop_factory is set)
|
|
147
|
+
self.agent_mode = AgentModeRunner(
|
|
148
|
+
agent_loop_factory=self.agent_loop_factory,
|
|
149
|
+
event_publisher=self.event_publisher,
|
|
150
|
+
artifact_processor=self._update_session_from_tool_results,
|
|
151
|
+
default_strategy=self.default_agent_strategy,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Initialize orchestrator
|
|
155
|
+
self.orchestrator = None # Will be initialized lazily to avoid circular dependency
|
|
156
|
+
|
|
157
|
+
def _get_orchestrator(self):
|
|
158
|
+
"""Lazy initialization of orchestrator."""
|
|
159
|
+
if self.orchestrator is None:
|
|
160
|
+
from .orchestrator import ChatOrchestrator
|
|
161
|
+
self.orchestrator = ChatOrchestrator(
|
|
162
|
+
llm=self.llm,
|
|
163
|
+
event_publisher=self.event_publisher,
|
|
164
|
+
session_repository=self.session_repository,
|
|
165
|
+
tool_manager=self.tool_manager,
|
|
166
|
+
prompt_provider=self.prompt_provider,
|
|
167
|
+
file_manager=self.file_manager,
|
|
168
|
+
artifact_processor=self._update_session_from_tool_results,
|
|
169
|
+
plain_mode=self.plain_mode,
|
|
170
|
+
rag_mode=self.rag_mode,
|
|
171
|
+
tools_mode=self.tools_mode,
|
|
172
|
+
agent_mode=self.agent_mode,
|
|
173
|
+
)
|
|
174
|
+
return self.orchestrator
|
|
175
|
+
|
|
176
|
+
async def create_session(
|
|
177
|
+
self,
|
|
178
|
+
session_id: UUID,
|
|
179
|
+
user_email: Optional[str] = None
|
|
180
|
+
) -> Session:
|
|
181
|
+
"""Create a new chat session."""
|
|
182
|
+
session = Session(id=session_id, user_email=user_email)
|
|
183
|
+
|
|
184
|
+
# Store in both legacy dict and new repository
|
|
185
|
+
self.sessions[session_id] = session
|
|
186
|
+
await self.session_repository.create(session)
|
|
187
|
+
|
|
188
|
+
logger.info(f"Created session {sanitize_for_logging(str(session_id))} for user {sanitize_for_logging(user_email)}")
|
|
189
|
+
return session
|
|
190
|
+
|
|
191
|
+
async def handle_chat_message(
|
|
192
|
+
self,
|
|
193
|
+
session_id: UUID,
|
|
194
|
+
content: str,
|
|
195
|
+
model: str,
|
|
196
|
+
selected_tools: Optional[List[str]] = None,
|
|
197
|
+
selected_prompts: Optional[List[str]] = None,
|
|
198
|
+
selected_data_sources: Optional[List[str]] = None,
|
|
199
|
+
only_rag: bool = False,
|
|
200
|
+
tool_choice_required: bool = False,
|
|
201
|
+
user_email: Optional[str] = None,
|
|
202
|
+
agent_mode: bool = False,
|
|
203
|
+
temperature: float = 0.7,
|
|
204
|
+
update_callback: Optional[UpdateCallback] = None,
|
|
205
|
+
**kwargs
|
|
206
|
+
) -> Dict[str, Any]:
|
|
207
|
+
"""
|
|
208
|
+
Handle incoming chat message - thin façade delegating to orchestrator.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
Response dictionary to send to client
|
|
212
|
+
"""
|
|
213
|
+
# Log non-sensitive metadata at INFO level for production monitoring
|
|
214
|
+
logger.info(
|
|
215
|
+
f"handle_chat_message called - session_id: {session_id}, "
|
|
216
|
+
f"model: {model}, content_length: {len(content)}, "
|
|
217
|
+
f"selected_tools: {selected_tools}, selected_prompts: {selected_prompts}, selected_data_sources: {selected_data_sources}, "
|
|
218
|
+
f"only_rag: {only_rag}, tool_choice_required: {tool_choice_required}, "
|
|
219
|
+
f"user_email: {sanitize_for_logging(user_email)}, agent_mode: {agent_mode}"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Log sensitive content only at DEBUG level for development/testing
|
|
223
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
224
|
+
content_preview = content[:100] + "..." if len(content) > 100 else content
|
|
225
|
+
sanitized_kwargs = error_handler.sanitize_kwargs_for_logging(kwargs)
|
|
226
|
+
logger.debug(
|
|
227
|
+
f"handle_chat_message content preview: '{sanitize_for_logging(content_preview)}', "
|
|
228
|
+
f"kwargs: {sanitized_kwargs}"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Get or create session
|
|
232
|
+
session = self.sessions.get(session_id)
|
|
233
|
+
if not session:
|
|
234
|
+
# Try session repository
|
|
235
|
+
session = await self.session_repository.get(session_id)
|
|
236
|
+
if not session:
|
|
237
|
+
await self.create_session(session_id, user_email)
|
|
238
|
+
else:
|
|
239
|
+
# Sync to legacy dict
|
|
240
|
+
self.sessions[session_id] = session
|
|
241
|
+
|
|
242
|
+
try:
|
|
243
|
+
# Delegate to orchestrator
|
|
244
|
+
orchestrator = self._get_orchestrator()
|
|
245
|
+
return await orchestrator.execute(
|
|
246
|
+
session_id=session_id,
|
|
247
|
+
content=content,
|
|
248
|
+
model=model,
|
|
249
|
+
user_email=user_email,
|
|
250
|
+
selected_tools=selected_tools,
|
|
251
|
+
selected_prompts=selected_prompts,
|
|
252
|
+
selected_data_sources=selected_data_sources,
|
|
253
|
+
only_rag=only_rag,
|
|
254
|
+
tool_choice_required=tool_choice_required,
|
|
255
|
+
agent_mode=agent_mode,
|
|
256
|
+
temperature=temperature,
|
|
257
|
+
update_callback=update_callback,
|
|
258
|
+
**kwargs
|
|
259
|
+
)
|
|
260
|
+
except DomainError:
|
|
261
|
+
# Let domain-level errors (e.g., LLM / rate limit / validation) bubble up
|
|
262
|
+
# so transport layers (WebSocket/HTTP) can handle them consistently.
|
|
263
|
+
raise
|
|
264
|
+
except Exception as e:
|
|
265
|
+
# Fallback for unexpected errors in HTTP-style callers
|
|
266
|
+
return error_handler.handle_chat_message_error(e, "chat message handling")
|
|
267
|
+
|
|
268
|
+
async def handle_reset_session(
|
|
269
|
+
self,
|
|
270
|
+
session_id: UUID,
|
|
271
|
+
user_email: Optional[str] = None
|
|
272
|
+
) -> Dict[str, Any]:
|
|
273
|
+
"""Handle session reset request from frontend."""
|
|
274
|
+
# End the current session
|
|
275
|
+
self.end_session(session_id)
|
|
276
|
+
|
|
277
|
+
# Create a new session
|
|
278
|
+
await self.create_session(session_id, user_email)
|
|
279
|
+
|
|
280
|
+
logger.info(f"Reset session {sanitize_for_logging(str(session_id))} for user {sanitize_for_logging(user_email)}")
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
"type": "session_reset",
|
|
284
|
+
"session_id": str(session_id),
|
|
285
|
+
"message": "New session created"
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async def handle_attach_file(
|
|
289
|
+
self,
|
|
290
|
+
session_id: UUID,
|
|
291
|
+
s3_key: str,
|
|
292
|
+
user_email: Optional[str] = None,
|
|
293
|
+
update_callback: Optional[UpdateCallback] = None
|
|
294
|
+
) -> Dict[str, Any]:
|
|
295
|
+
"""Attach a file from library to the current session."""
|
|
296
|
+
session = self.sessions.get(session_id)
|
|
297
|
+
if not session:
|
|
298
|
+
session = await self.create_session(session_id, user_email)
|
|
299
|
+
|
|
300
|
+
# Verify the file exists and belongs to the user
|
|
301
|
+
if not self.file_manager or not user_email:
|
|
302
|
+
return {
|
|
303
|
+
"type": "file_attach",
|
|
304
|
+
"s3_key": s3_key,
|
|
305
|
+
"success": False,
|
|
306
|
+
"error": "File manager not available or no user email"
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
# Get file metadata
|
|
311
|
+
file_result = await self.file_manager.s3_client.get_file(user_email, s3_key)
|
|
312
|
+
if not file_result:
|
|
313
|
+
return {
|
|
314
|
+
"type": "file_attach",
|
|
315
|
+
"s3_key": s3_key,
|
|
316
|
+
"success": False,
|
|
317
|
+
"error": "File not found"
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
filename = file_result.get("filename")
|
|
321
|
+
if not filename:
|
|
322
|
+
return {
|
|
323
|
+
"type": "file_attach",
|
|
324
|
+
"s3_key": s3_key,
|
|
325
|
+
"success": False,
|
|
326
|
+
"error": "Invalid file metadata"
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
# Add file reference directly to session context (file already exists in S3)
|
|
330
|
+
session.context.setdefault("files", {})[filename] = {
|
|
331
|
+
"key": s3_key,
|
|
332
|
+
"content_type": file_result.get("content_type"),
|
|
333
|
+
"size": file_result.get("size"),
|
|
334
|
+
"source": "user",
|
|
335
|
+
"last_modified": file_result.get("last_modified"),
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
sanitized_s3_key = s3_key.replace('\r', '').replace('\n', '')
|
|
339
|
+
logger.info(f"Attached file ({sanitized_s3_key}) to session {session_id}")
|
|
340
|
+
|
|
341
|
+
# Emit files_update to notify UI
|
|
342
|
+
if update_callback:
|
|
343
|
+
await file_processor.emit_files_update_from_context(
|
|
344
|
+
session_context=session.context,
|
|
345
|
+
file_manager=self.file_manager,
|
|
346
|
+
update_callback=update_callback
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
"type": "file_attach",
|
|
351
|
+
"s3_key": s3_key,
|
|
352
|
+
"filename": filename,
|
|
353
|
+
"success": True,
|
|
354
|
+
"message": f"File {filename} attached to session"
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
except Exception as e:
|
|
358
|
+
safe_key = s3_key.replace('\n', '').replace('\r', '')
|
|
359
|
+
safe_err = str(e).replace('\n', '').replace('\r', '')
|
|
360
|
+
logger.error(f"Failed to attach file {safe_key} to session {session_id}: {safe_err}")
|
|
361
|
+
return {
|
|
362
|
+
"type": "file_attach",
|
|
363
|
+
"s3_key": s3_key,
|
|
364
|
+
"success": False,
|
|
365
|
+
"error": str(e)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async def handle_download_file(
|
|
369
|
+
self,
|
|
370
|
+
session_id: UUID,
|
|
371
|
+
filename: str,
|
|
372
|
+
user_email: Optional[str]
|
|
373
|
+
) -> Dict[str, Any]:
|
|
374
|
+
"""Download a file by original filename (within session context)."""
|
|
375
|
+
session = self.sessions.get(session_id)
|
|
376
|
+
if not session or not self.file_manager or not user_email:
|
|
377
|
+
return {
|
|
378
|
+
"type": MessageType.FILE_DOWNLOAD.value,
|
|
379
|
+
"filename": filename,
|
|
380
|
+
"error": "Session or file manager not available"
|
|
381
|
+
}
|
|
382
|
+
ref = session.context.get("files", {}).get(filename)
|
|
383
|
+
if not ref:
|
|
384
|
+
return {
|
|
385
|
+
"type": MessageType.FILE_DOWNLOAD.value,
|
|
386
|
+
"filename": filename,
|
|
387
|
+
"error": "File not found in session"
|
|
388
|
+
}
|
|
389
|
+
try:
|
|
390
|
+
content_b64 = await self.file_manager.get_file_content(
|
|
391
|
+
user_email=user_email,
|
|
392
|
+
filename=filename,
|
|
393
|
+
s3_key=ref.get("key")
|
|
394
|
+
)
|
|
395
|
+
if not content_b64:
|
|
396
|
+
return {
|
|
397
|
+
"type": MessageType.FILE_DOWNLOAD.value,
|
|
398
|
+
"filename": filename,
|
|
399
|
+
"error": "Unable to retrieve file content"
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
"type": MessageType.FILE_DOWNLOAD.value,
|
|
403
|
+
"filename": filename,
|
|
404
|
+
"content_base64": content_b64
|
|
405
|
+
}
|
|
406
|
+
except Exception as e:
|
|
407
|
+
logger.error(f"Download failed for {filename}: {e}")
|
|
408
|
+
return {
|
|
409
|
+
"type": MessageType.FILE_DOWNLOAD.value,
|
|
410
|
+
"filename": filename,
|
|
411
|
+
"error": str(e)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async def _update_session_from_tool_results(
|
|
415
|
+
self,
|
|
416
|
+
session: Session,
|
|
417
|
+
tool_results: List[ToolResult],
|
|
418
|
+
update_callback: Optional[UpdateCallback]
|
|
419
|
+
) -> None:
|
|
420
|
+
"""Persist tool artifacts, update session context, and notify UI for canvas."""
|
|
421
|
+
if not tool_results:
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
if not self.file_manager:
|
|
425
|
+
logger.info("No file_manager configured; skipping artifact ingestion")
|
|
426
|
+
return
|
|
427
|
+
|
|
428
|
+
# Build a working session context including user email
|
|
429
|
+
session_context: Dict[str, Any] = build_session_context(session)
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
for result in tool_results:
|
|
433
|
+
# Ingest v2 artifacts and emit files_update + canvas_files (with display hints)
|
|
434
|
+
session_context = await file_processor.process_tool_artifacts(
|
|
435
|
+
session_context=session_context,
|
|
436
|
+
tool_result=result,
|
|
437
|
+
file_manager=self.file_manager,
|
|
438
|
+
update_callback=update_callback
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# Persist updated context back to the session
|
|
442
|
+
session.context.update({k: v for k, v in session_context.items() if k != "session_id"})
|
|
443
|
+
except Exception as e:
|
|
444
|
+
logger.error(f"Failed to update session from tool results: {e}", exc_info=True)
|
|
445
|
+
|
|
446
|
+
def get_session(self, session_id: UUID) -> Optional[Session]:
|
|
447
|
+
"""Get session by ID."""
|
|
448
|
+
return self.sessions.get(session_id)
|
|
449
|
+
|
|
450
|
+
def end_session(self, session_id: UUID) -> None:
|
|
451
|
+
"""End a session."""
|
|
452
|
+
if session_id in self.sessions:
|
|
453
|
+
self.sessions[session_id].active = False
|
|
454
|
+
logger.info(f"Ended session {sanitize_for_logging(str(session_id))}")
|