atlas-chat 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. atlas/__init__.py +40 -0
  2. atlas/application/__init__.py +7 -0
  3. atlas/application/chat/__init__.py +7 -0
  4. atlas/application/chat/agent/__init__.py +10 -0
  5. atlas/application/chat/agent/act_loop.py +179 -0
  6. atlas/application/chat/agent/factory.py +142 -0
  7. atlas/application/chat/agent/protocols.py +46 -0
  8. atlas/application/chat/agent/react_loop.py +338 -0
  9. atlas/application/chat/agent/think_act_loop.py +171 -0
  10. atlas/application/chat/approval_manager.py +151 -0
  11. atlas/application/chat/elicitation_manager.py +191 -0
  12. atlas/application/chat/events/__init__.py +1 -0
  13. atlas/application/chat/events/agent_event_relay.py +112 -0
  14. atlas/application/chat/modes/__init__.py +1 -0
  15. atlas/application/chat/modes/agent.py +125 -0
  16. atlas/application/chat/modes/plain.py +74 -0
  17. atlas/application/chat/modes/rag.py +81 -0
  18. atlas/application/chat/modes/tools.py +179 -0
  19. atlas/application/chat/orchestrator.py +213 -0
  20. atlas/application/chat/policies/__init__.py +1 -0
  21. atlas/application/chat/policies/tool_authorization.py +99 -0
  22. atlas/application/chat/preprocessors/__init__.py +1 -0
  23. atlas/application/chat/preprocessors/message_builder.py +92 -0
  24. atlas/application/chat/preprocessors/prompt_override_service.py +104 -0
  25. atlas/application/chat/service.py +454 -0
  26. atlas/application/chat/utilities/__init__.py +6 -0
  27. atlas/application/chat/utilities/error_handler.py +367 -0
  28. atlas/application/chat/utilities/event_notifier.py +546 -0
  29. atlas/application/chat/utilities/file_processor.py +613 -0
  30. atlas/application/chat/utilities/tool_executor.py +789 -0
  31. atlas/atlas_chat_cli.py +347 -0
  32. atlas/atlas_client.py +238 -0
  33. atlas/core/__init__.py +0 -0
  34. atlas/core/auth.py +205 -0
  35. atlas/core/authorization_manager.py +27 -0
  36. atlas/core/capabilities.py +123 -0
  37. atlas/core/compliance.py +215 -0
  38. atlas/core/domain_whitelist.py +147 -0
  39. atlas/core/domain_whitelist_middleware.py +82 -0
  40. atlas/core/http_client.py +28 -0
  41. atlas/core/log_sanitizer.py +102 -0
  42. atlas/core/metrics_logger.py +59 -0
  43. atlas/core/middleware.py +131 -0
  44. atlas/core/otel_config.py +242 -0
  45. atlas/core/prompt_risk.py +200 -0
  46. atlas/core/rate_limit.py +0 -0
  47. atlas/core/rate_limit_middleware.py +64 -0
  48. atlas/core/security_headers_middleware.py +51 -0
  49. atlas/domain/__init__.py +37 -0
  50. atlas/domain/chat/__init__.py +1 -0
  51. atlas/domain/chat/dtos.py +85 -0
  52. atlas/domain/errors.py +96 -0
  53. atlas/domain/messages/__init__.py +12 -0
  54. atlas/domain/messages/models.py +160 -0
  55. atlas/domain/rag_mcp_service.py +664 -0
  56. atlas/domain/sessions/__init__.py +7 -0
  57. atlas/domain/sessions/models.py +36 -0
  58. atlas/domain/unified_rag_service.py +371 -0
  59. atlas/infrastructure/__init__.py +10 -0
  60. atlas/infrastructure/app_factory.py +135 -0
  61. atlas/infrastructure/events/__init__.py +1 -0
  62. atlas/infrastructure/events/cli_event_publisher.py +140 -0
  63. atlas/infrastructure/events/websocket_publisher.py +140 -0
  64. atlas/infrastructure/sessions/in_memory_repository.py +56 -0
  65. atlas/infrastructure/transport/__init__.py +7 -0
  66. atlas/infrastructure/transport/websocket_connection_adapter.py +33 -0
  67. atlas/init_cli.py +226 -0
  68. atlas/interfaces/__init__.py +15 -0
  69. atlas/interfaces/events.py +134 -0
  70. atlas/interfaces/llm.py +54 -0
  71. atlas/interfaces/rag.py +40 -0
  72. atlas/interfaces/sessions.py +75 -0
  73. atlas/interfaces/tools.py +57 -0
  74. atlas/interfaces/transport.py +24 -0
  75. atlas/main.py +564 -0
  76. atlas/mcp/api_key_demo/README.md +76 -0
  77. atlas/mcp/api_key_demo/main.py +172 -0
  78. atlas/mcp/api_key_demo/run.sh +56 -0
  79. atlas/mcp/basictable/main.py +147 -0
  80. atlas/mcp/calculator/main.py +149 -0
  81. atlas/mcp/code-executor/execution_engine.py +98 -0
  82. atlas/mcp/code-executor/execution_environment.py +95 -0
  83. atlas/mcp/code-executor/main.py +528 -0
  84. atlas/mcp/code-executor/result_processing.py +276 -0
  85. atlas/mcp/code-executor/script_generation.py +195 -0
  86. atlas/mcp/code-executor/security_checker.py +140 -0
  87. atlas/mcp/corporate_cars/main.py +437 -0
  88. atlas/mcp/csv_reporter/main.py +545 -0
  89. atlas/mcp/duckduckgo/main.py +182 -0
  90. atlas/mcp/elicitation_demo/README.md +171 -0
  91. atlas/mcp/elicitation_demo/main.py +262 -0
  92. atlas/mcp/env-demo/README.md +158 -0
  93. atlas/mcp/env-demo/main.py +199 -0
  94. atlas/mcp/file_size_test/main.py +284 -0
  95. atlas/mcp/filesystem/main.py +348 -0
  96. atlas/mcp/image_demo/main.py +113 -0
  97. atlas/mcp/image_demo/requirements.txt +4 -0
  98. atlas/mcp/logging_demo/README.md +72 -0
  99. atlas/mcp/logging_demo/main.py +103 -0
  100. atlas/mcp/many_tools_demo/main.py +50 -0
  101. atlas/mcp/order_database/__init__.py +0 -0
  102. atlas/mcp/order_database/main.py +369 -0
  103. atlas/mcp/order_database/signal_data.csv +1001 -0
  104. atlas/mcp/pdfbasic/main.py +394 -0
  105. atlas/mcp/pptx_generator/main.py +760 -0
  106. atlas/mcp/pptx_generator/requirements.txt +13 -0
  107. atlas/mcp/pptx_generator/run_test.sh +1 -0
  108. atlas/mcp/pptx_generator/test_pptx_generator_security.py +169 -0
  109. atlas/mcp/progress_demo/main.py +167 -0
  110. atlas/mcp/progress_updates_demo/QUICKSTART.md +273 -0
  111. atlas/mcp/progress_updates_demo/README.md +120 -0
  112. atlas/mcp/progress_updates_demo/main.py +497 -0
  113. atlas/mcp/prompts/main.py +222 -0
  114. atlas/mcp/public_demo/main.py +189 -0
  115. atlas/mcp/sampling_demo/README.md +169 -0
  116. atlas/mcp/sampling_demo/main.py +234 -0
  117. atlas/mcp/thinking/main.py +77 -0
  118. atlas/mcp/tool_planner/main.py +240 -0
  119. atlas/mcp/ui-demo/badmesh.png +0 -0
  120. atlas/mcp/ui-demo/main.py +383 -0
  121. atlas/mcp/ui-demo/templates/button_demo.html +32 -0
  122. atlas/mcp/ui-demo/templates/data_visualization.html +32 -0
  123. atlas/mcp/ui-demo/templates/form_demo.html +28 -0
  124. atlas/mcp/username-override-demo/README.md +320 -0
  125. atlas/mcp/username-override-demo/main.py +308 -0
  126. atlas/modules/__init__.py +0 -0
  127. atlas/modules/config/__init__.py +34 -0
  128. atlas/modules/config/cli.py +231 -0
  129. atlas/modules/config/config_manager.py +1096 -0
  130. atlas/modules/file_storage/__init__.py +22 -0
  131. atlas/modules/file_storage/cli.py +330 -0
  132. atlas/modules/file_storage/content_extractor.py +290 -0
  133. atlas/modules/file_storage/manager.py +295 -0
  134. atlas/modules/file_storage/mock_s3_client.py +402 -0
  135. atlas/modules/file_storage/s3_client.py +417 -0
  136. atlas/modules/llm/__init__.py +19 -0
  137. atlas/modules/llm/caller.py +287 -0
  138. atlas/modules/llm/litellm_caller.py +675 -0
  139. atlas/modules/llm/models.py +19 -0
  140. atlas/modules/mcp_tools/__init__.py +17 -0
  141. atlas/modules/mcp_tools/client.py +2123 -0
  142. atlas/modules/mcp_tools/token_storage.py +556 -0
  143. atlas/modules/prompts/prompt_provider.py +130 -0
  144. atlas/modules/rag/__init__.py +24 -0
  145. atlas/modules/rag/atlas_rag_client.py +336 -0
  146. atlas/modules/rag/client.py +129 -0
  147. atlas/routes/admin_routes.py +865 -0
  148. atlas/routes/config_routes.py +484 -0
  149. atlas/routes/feedback_routes.py +361 -0
  150. atlas/routes/files_routes.py +274 -0
  151. atlas/routes/health_routes.py +40 -0
  152. atlas/routes/mcp_auth_routes.py +223 -0
  153. atlas/server_cli.py +164 -0
  154. atlas/tests/conftest.py +20 -0
  155. atlas/tests/integration/test_mcp_auth_integration.py +152 -0
  156. atlas/tests/manual_test_sampling.py +87 -0
  157. atlas/tests/modules/mcp_tools/test_client_auth.py +226 -0
  158. atlas/tests/modules/mcp_tools/test_client_env.py +191 -0
  159. atlas/tests/test_admin_mcp_server_management_routes.py +141 -0
  160. atlas/tests/test_agent_roa.py +135 -0
  161. atlas/tests/test_app_factory_smoke.py +47 -0
  162. atlas/tests/test_approval_manager.py +439 -0
  163. atlas/tests/test_atlas_client.py +188 -0
  164. atlas/tests/test_atlas_rag_client.py +447 -0
  165. atlas/tests/test_atlas_rag_integration.py +224 -0
  166. atlas/tests/test_attach_file_flow.py +287 -0
  167. atlas/tests/test_auth_utils.py +165 -0
  168. atlas/tests/test_backend_public_url.py +185 -0
  169. atlas/tests/test_banner_logging.py +287 -0
  170. atlas/tests/test_capability_tokens_and_injection.py +203 -0
  171. atlas/tests/test_compliance_level.py +54 -0
  172. atlas/tests/test_compliance_manager.py +253 -0
  173. atlas/tests/test_config_manager.py +617 -0
  174. atlas/tests/test_config_manager_paths.py +12 -0
  175. atlas/tests/test_core_auth.py +18 -0
  176. atlas/tests/test_core_utils.py +190 -0
  177. atlas/tests/test_docker_env_sync.py +202 -0
  178. atlas/tests/test_domain_errors.py +329 -0
  179. atlas/tests/test_domain_whitelist.py +359 -0
  180. atlas/tests/test_elicitation_manager.py +408 -0
  181. atlas/tests/test_elicitation_routing.py +296 -0
  182. atlas/tests/test_env_demo_server.py +88 -0
  183. atlas/tests/test_error_classification.py +113 -0
  184. atlas/tests/test_error_flow_integration.py +116 -0
  185. atlas/tests/test_feedback_routes.py +333 -0
  186. atlas/tests/test_file_content_extraction.py +1134 -0
  187. atlas/tests/test_file_extraction_routes.py +158 -0
  188. atlas/tests/test_file_library.py +107 -0
  189. atlas/tests/test_file_manager_unit.py +18 -0
  190. atlas/tests/test_health_route.py +49 -0
  191. atlas/tests/test_http_client_stub.py +8 -0
  192. atlas/tests/test_imports_smoke.py +30 -0
  193. atlas/tests/test_interfaces_llm_response.py +9 -0
  194. atlas/tests/test_issue_access_denied_fix.py +136 -0
  195. atlas/tests/test_llm_env_expansion.py +836 -0
  196. atlas/tests/test_log_level_sensitive_data.py +285 -0
  197. atlas/tests/test_mcp_auth_routes.py +341 -0
  198. atlas/tests/test_mcp_client_auth.py +331 -0
  199. atlas/tests/test_mcp_data_injection.py +270 -0
  200. atlas/tests/test_mcp_get_authorized_servers.py +95 -0
  201. atlas/tests/test_mcp_hot_reload.py +512 -0
  202. atlas/tests/test_mcp_image_content.py +424 -0
  203. atlas/tests/test_mcp_logging.py +172 -0
  204. atlas/tests/test_mcp_progress_updates.py +313 -0
  205. atlas/tests/test_mcp_prompt_override_system_prompt.py +102 -0
  206. atlas/tests/test_mcp_prompts_server.py +39 -0
  207. atlas/tests/test_mcp_tool_result_parsing.py +296 -0
  208. atlas/tests/test_metrics_logger.py +56 -0
  209. atlas/tests/test_middleware_auth.py +379 -0
  210. atlas/tests/test_prompt_risk_and_acl.py +141 -0
  211. atlas/tests/test_rag_mcp_aggregator.py +204 -0
  212. atlas/tests/test_rag_mcp_service.py +224 -0
  213. atlas/tests/test_rate_limit_middleware.py +45 -0
  214. atlas/tests/test_routes_config_smoke.py +60 -0
  215. atlas/tests/test_routes_files_download_token.py +41 -0
  216. atlas/tests/test_routes_files_health.py +18 -0
  217. atlas/tests/test_runtime_imports.py +53 -0
  218. atlas/tests/test_sampling_integration.py +482 -0
  219. atlas/tests/test_security_admin_routes.py +61 -0
  220. atlas/tests/test_security_capability_tokens.py +65 -0
  221. atlas/tests/test_security_file_stats_scope.py +21 -0
  222. atlas/tests/test_security_header_injection.py +191 -0
  223. atlas/tests/test_security_headers_and_filename.py +63 -0
  224. atlas/tests/test_shared_session_repository.py +101 -0
  225. atlas/tests/test_system_prompt_loading.py +181 -0
  226. atlas/tests/test_token_storage.py +505 -0
  227. atlas/tests/test_tool_approval_config.py +93 -0
  228. atlas/tests/test_tool_approval_utils.py +356 -0
  229. atlas/tests/test_tool_authorization_group_filtering.py +223 -0
  230. atlas/tests/test_tool_details_in_config.py +108 -0
  231. atlas/tests/test_tool_planner.py +300 -0
  232. atlas/tests/test_unified_rag_service.py +398 -0
  233. atlas/tests/test_username_override_in_approval.py +258 -0
  234. atlas/tests/test_websocket_auth_header.py +168 -0
  235. atlas/version.py +6 -0
  236. atlas_chat-0.1.0.data/data/.env.example +253 -0
  237. atlas_chat-0.1.0.data/data/config/defaults/compliance-levels.json +44 -0
  238. atlas_chat-0.1.0.data/data/config/defaults/domain-whitelist.json +123 -0
  239. atlas_chat-0.1.0.data/data/config/defaults/file-extractors.json +74 -0
  240. atlas_chat-0.1.0.data/data/config/defaults/help-config.json +198 -0
  241. atlas_chat-0.1.0.data/data/config/defaults/llmconfig-buggy.yml +11 -0
  242. atlas_chat-0.1.0.data/data/config/defaults/llmconfig.yml +19 -0
  243. atlas_chat-0.1.0.data/data/config/defaults/mcp.json +138 -0
  244. atlas_chat-0.1.0.data/data/config/defaults/rag-sources.json +17 -0
  245. atlas_chat-0.1.0.data/data/config/defaults/splash-config.json +16 -0
  246. atlas_chat-0.1.0.dist-info/METADATA +236 -0
  247. atlas_chat-0.1.0.dist-info/RECORD +250 -0
  248. atlas_chat-0.1.0.dist-info/WHEEL +5 -0
  249. atlas_chat-0.1.0.dist-info/entry_points.txt +4 -0
  250. atlas_chat-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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))}")
@@ -0,0 +1,6 @@
1
+ """
2
+ Chat utilities module - pure functions for chat operations.
3
+
4
+ This module provides stateless utility functions that can be used
5
+ by the ChatService to handle various operations without tight coupling.
6
+ """