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
atlas/__init__.py ADDED
@@ -0,0 +1,40 @@
1
+ """
2
+ Atlas - Full-stack LLM chat interface with MCP integration.
3
+
4
+ This package provides both a Python API for programmatic access and
5
+ CLI tools for interacting with LLMs.
6
+
7
+ Example usage:
8
+ from atlas import AtlasClient, ChatResult
9
+
10
+ client = AtlasClient()
11
+ result = await client.chat("Hello, world!")
12
+ print(result.message)
13
+
14
+ CLI tools (after pip install):
15
+ atlas-chat "Your prompt here" --model gpt-4o
16
+ atlas-server --port 8000
17
+ """
18
+
19
+ from atlas.version import VERSION
20
+
21
+ __version__ = VERSION
22
+ __all__ = [
23
+ "AtlasClient",
24
+ "ChatResult",
25
+ "VERSION",
26
+ "__version__",
27
+ ]
28
+
29
+
30
+ def __getattr__(name: str):
31
+ """Lazy import to avoid loading heavy dependencies at module import time."""
32
+ if name == "AtlasClient":
33
+ from atlas.atlas_client import AtlasClient
34
+ globals()["AtlasClient"] = AtlasClient # Cache for subsequent accesses
35
+ return AtlasClient
36
+ if name == "ChatResult":
37
+ from atlas.atlas_client import ChatResult
38
+ globals()["ChatResult"] = ChatResult # Cache for subsequent accesses
39
+ return ChatResult
40
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,7 @@
1
+ """Application layer - business logic and use cases."""
2
+
3
+ from .chat.service import ChatService
4
+
5
+ __all__ = [
6
+ "ChatService",
7
+ ]
@@ -0,0 +1,7 @@
1
+ """Chat application service."""
2
+
3
+ from .service import ChatService
4
+
5
+ __all__ = [
6
+ "ChatService",
7
+ ]
@@ -0,0 +1,10 @@
1
+ # Agent loop package exports
2
+
3
+ from .factory import AgentLoopFactory as AgentLoopFactory
4
+ from .protocols import AgentContext as AgentContext
5
+ from .protocols import AgentEvent as AgentEvent
6
+ from .protocols import AgentEventHandler as AgentEventHandler
7
+ from .protocols import AgentLoopProtocol as AgentLoopProtocol
8
+ from .protocols import AgentResult as AgentResult
9
+ from .react_loop import ReActAgentLoop as ReActAgentLoop
10
+ from .think_act_loop import ThinkActAgentLoop as ThinkActAgentLoop
@@ -0,0 +1,179 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from atlas.interfaces.llm import LLMProtocol
7
+ from atlas.interfaces.tools import ToolManagerProtocol
8
+ from atlas.modules.prompts.prompt_provider import PromptProvider
9
+
10
+ from ..utilities import error_handler, tool_executor
11
+ from .protocols import AgentContext, AgentEvent, AgentEventHandler, AgentLoopProtocol, AgentResult
12
+
13
+
14
+ class ActAgentLoop(AgentLoopProtocol):
15
+ """Pure action agent loop - just execute tools in a loop until done.
16
+
17
+ No explicit reasoning or observation steps. The LLM directly decides which
18
+ tools to call and when to finish. Fastest strategy with minimal overhead.
19
+
20
+ Exit conditions:
21
+ - LLM calls the "finished" tool with a final_answer
22
+ - No tool calls returned (LLM provides text response)
23
+ - Max steps reached
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ *,
29
+ llm: LLMProtocol,
30
+ tool_manager: Optional[ToolManagerProtocol],
31
+ prompt_provider: Optional[PromptProvider],
32
+ connection: Any = None,
33
+ config_manager=None,
34
+ ) -> None:
35
+ self.llm = llm
36
+ self.tool_manager = tool_manager
37
+ self.prompt_provider = prompt_provider
38
+ self.connection = connection
39
+ self.config_manager = config_manager
40
+ self.skip_approval = False
41
+
42
+ def _extract_finished_args(self, tool_calls: List[Dict[str, Any]]) -> Optional[str]:
43
+ """Extract final_answer from finished tool call if present."""
44
+ try:
45
+ for tc in tool_calls:
46
+ f = tc.get("function") if isinstance(tc, dict) else None
47
+ if f and f.get("name") == "finished":
48
+ raw_args = f.get("arguments")
49
+ if isinstance(raw_args, str):
50
+ try:
51
+ args = json.loads(raw_args)
52
+ return args.get("final_answer")
53
+ except Exception:
54
+ return None
55
+ if isinstance(raw_args, dict):
56
+ return raw_args.get("final_answer")
57
+ return None
58
+ except Exception:
59
+ return None
60
+
61
+ async def run(
62
+ self,
63
+ *,
64
+ model: str,
65
+ messages: List[Dict[str, Any]],
66
+ context: AgentContext,
67
+ selected_tools: Optional[List[str]],
68
+ data_sources: Optional[List[str]],
69
+ max_steps: int,
70
+ temperature: float,
71
+ event_handler: AgentEventHandler,
72
+ ) -> AgentResult:
73
+ await event_handler(AgentEvent(type="agent_start", payload={"max_steps": max_steps, "strategy": "act"}))
74
+
75
+ steps = 0
76
+ final_answer: Optional[str] = None
77
+
78
+ # Define the "finished" control tool
79
+ finished_tool_schema = {
80
+ "type": "function",
81
+ "function": {
82
+ "name": "finished",
83
+ "description": "Call this when you have completed the task and are ready to provide a final answer to the user.",
84
+ "parameters": {
85
+ "type": "object",
86
+ "properties": {
87
+ "final_answer": {
88
+ "type": "string",
89
+ "description": "The final response to provide to the user",
90
+ },
91
+ },
92
+ "required": ["final_answer"],
93
+ "additionalProperties": False,
94
+ },
95
+ },
96
+ }
97
+
98
+ while steps < max_steps and final_answer is None:
99
+ steps += 1
100
+ await event_handler(AgentEvent(type="agent_turn_start", payload={"step": steps}))
101
+
102
+ # Build tools schema: user tools + finished tool
103
+ tools_schema: List[Dict[str, Any]] = [finished_tool_schema]
104
+ if selected_tools and self.tool_manager:
105
+ user_tools = await error_handler.safe_get_tools_schema(self.tool_manager, selected_tools)
106
+ tools_schema.extend(user_tools)
107
+
108
+ # Call LLM with tools - using "required" to force tool calling during Act phase
109
+ # The LiteLLM caller has fallback logic to "auto" if "required" is not supported
110
+ if data_sources and context.user_email:
111
+ llm_response = await self.llm.call_with_rag_and_tools(
112
+ model, messages, data_sources, tools_schema, context.user_email, "required", temperature=temperature
113
+ )
114
+ else:
115
+ llm_response = await self.llm.call_with_tools(
116
+ model, messages, tools_schema, "required", temperature=temperature
117
+ )
118
+
119
+ # Process response
120
+ if llm_response.has_tool_calls():
121
+ tool_calls = llm_response.tool_calls or []
122
+
123
+ # Check if finished tool was called
124
+ final_answer = self._extract_finished_args(tool_calls)
125
+ if final_answer:
126
+ break
127
+
128
+ # Execute first non-finished tool call
129
+ first_call = None
130
+ for tc in tool_calls:
131
+ f = tc.get("function") if isinstance(tc, dict) else None
132
+ if f and f.get("name") != "finished":
133
+ first_call = tc
134
+ break
135
+
136
+ if first_call is None:
137
+ # Only finished tool or no valid tools
138
+ final_answer = llm_response.content or "Task completed."
139
+ break
140
+
141
+ # Execute the tool
142
+ messages.append({
143
+ "role": "assistant",
144
+ "content": llm_response.content,
145
+ "tool_calls": [first_call],
146
+ })
147
+
148
+ result = await tool_executor.execute_single_tool(
149
+ tool_call=first_call,
150
+ session_context={
151
+ "session_id": context.session_id,
152
+ "user_email": context.user_email,
153
+ "files": context.files,
154
+ },
155
+ tool_manager=self.tool_manager,
156
+ update_callback=(self.connection.send_json if self.connection else None),
157
+ config_manager=self.config_manager,
158
+ skip_approval=self.skip_approval,
159
+ )
160
+
161
+ messages.append({
162
+ "role": "tool",
163
+ "content": result.content,
164
+ "tool_call_id": result.tool_call_id,
165
+ })
166
+
167
+ # Emit tool results for artifact ingestion
168
+ await event_handler(AgentEvent(type="agent_tool_results", payload={"results": [result]}))
169
+ else:
170
+ # No tool calls - treat content as final answer
171
+ final_answer = llm_response.content or "Task completed."
172
+ break
173
+
174
+ # Fallback if no final answer after max steps
175
+ if not final_answer:
176
+ final_answer = await self.llm.call_plain(model, messages, temperature=temperature)
177
+
178
+ await event_handler(AgentEvent(type="agent_completion", payload={"steps": steps}))
179
+ return AgentResult(final_answer=final_answer, steps=steps, metadata={"agent_mode": True, "strategy": "act"})
@@ -0,0 +1,142 @@
1
+ """Factory for creating agent loop instances based on strategy."""
2
+
3
+ import logging
4
+ from typing import Optional
5
+
6
+ from atlas.interfaces.llm import LLMProtocol
7
+ from atlas.interfaces.tools import ToolManagerProtocol
8
+ from atlas.interfaces.transport import ChatConnectionProtocol
9
+ from atlas.modules.prompts.prompt_provider import PromptProvider
10
+
11
+ from .act_loop import ActAgentLoop
12
+ from .protocols import AgentLoopProtocol
13
+ from .react_loop import ReActAgentLoop
14
+ from .think_act_loop import ThinkActAgentLoop
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class AgentLoopFactory:
20
+ """
21
+ Factory for creating agent loop instances.
22
+
23
+ This factory pattern allows for easy addition of new agent loop strategies
24
+ without modifying existing code. Simply add a new strategy to the registry.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ llm: LLMProtocol,
30
+ tool_manager: Optional[ToolManagerProtocol] = None,
31
+ prompt_provider: Optional[PromptProvider] = None,
32
+ connection: Optional[ChatConnectionProtocol] = None,
33
+ config_manager=None,
34
+ ):
35
+ """
36
+ Initialize factory with shared dependencies.
37
+
38
+ Args:
39
+ llm: LLM protocol implementation
40
+ tool_manager: Optional tool manager
41
+ prompt_provider: Optional prompt provider
42
+ connection: Optional connection for sending updates
43
+ config_manager: Optional config manager for approval settings
44
+ """
45
+ self.llm = llm
46
+ self.tool_manager = tool_manager
47
+ self.prompt_provider = prompt_provider
48
+ self.connection = connection
49
+ self.config_manager = config_manager
50
+ self.skip_approval = False
51
+
52
+ # Registry of available strategies
53
+ self._strategy_registry = {
54
+ "react": ReActAgentLoop,
55
+ "think-act": ThinkActAgentLoop,
56
+ "think_act": ThinkActAgentLoop,
57
+ "thinkact": ThinkActAgentLoop,
58
+ "act": ActAgentLoop,
59
+ }
60
+
61
+ # Cache of instantiated loops for performance
62
+ self._loop_cache: dict[str, AgentLoopProtocol] = {}
63
+
64
+ def create(self, strategy: str = "think-act") -> AgentLoopProtocol:
65
+ """
66
+ Create an agent loop instance for the given strategy.
67
+
68
+ Args:
69
+ strategy: Strategy name (react, think-act, act, etc.)
70
+
71
+ Returns:
72
+ AgentLoopProtocol instance
73
+
74
+ Note:
75
+ If the strategy is not recognized, falls back to 'react' with a warning.
76
+ """
77
+ strategy_normalized = strategy.lower().strip()
78
+
79
+ # Check cache first
80
+ if strategy_normalized in self._loop_cache:
81
+ logger.info(f"Using agent loop strategy: {strategy_normalized}")
82
+ return self._loop_cache[strategy_normalized]
83
+
84
+ # Look up strategy in registry
85
+ loop_class = self._strategy_registry.get(strategy_normalized)
86
+
87
+ if loop_class is None:
88
+ logger.warning(
89
+ f"Unknown agent loop strategy '{strategy}', falling back to 'react'"
90
+ )
91
+ loop_class = self._strategy_registry["react"]
92
+ strategy_normalized = "react"
93
+
94
+ # Instantiate the loop
95
+ loop_instance = loop_class(
96
+ llm=self.llm,
97
+ tool_manager=self.tool_manager,
98
+ prompt_provider=self.prompt_provider,
99
+ connection=self.connection,
100
+ config_manager=self.config_manager,
101
+ )
102
+
103
+ loop_instance.skip_approval = self.skip_approval
104
+
105
+ # Cache for future use
106
+ self._loop_cache[strategy_normalized] = loop_instance
107
+
108
+ logger.info(f"Created and using agent loop strategy: {strategy_normalized}")
109
+ return loop_instance
110
+
111
+ def get_available_strategies(self) -> list[str]:
112
+ """
113
+ Get list of available strategy names.
114
+
115
+ Returns:
116
+ List of strategy identifiers
117
+ """
118
+ # Return unique strategy names (deduplicated)
119
+ unique_strategies = set()
120
+ for strategy in self._strategy_registry.keys():
121
+ # Normalize to primary name
122
+ if strategy in ("react",):
123
+ unique_strategies.add("react")
124
+ elif strategy in ("think-act", "think_act", "thinkact"):
125
+ unique_strategies.add("think-act")
126
+ elif strategy in ("act",):
127
+ unique_strategies.add("act")
128
+ return sorted(unique_strategies)
129
+
130
+ def register_strategy(self, name: str, loop_class: type[AgentLoopProtocol]) -> None:
131
+ """
132
+ Register a new agent loop strategy.
133
+
134
+ This allows for dynamic extension of available strategies.
135
+
136
+ Args:
137
+ name: Strategy identifier
138
+ loop_class: Agent loop class to instantiate
139
+ """
140
+ name_normalized = name.lower().strip()
141
+ self._strategy_registry[name_normalized] = loop_class
142
+ logger.info(f"Registered new agent loop strategy: {name_normalized}")
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Awaitable, Callable, Dict, List, Optional, Protocol
5
+ from uuid import UUID
6
+
7
+ from atlas.domain.messages.models import ConversationHistory
8
+
9
+
10
+ @dataclass
11
+ class AgentContext:
12
+ session_id: UUID
13
+ user_email: Optional[str]
14
+ files: Dict[str, Any]
15
+ history: ConversationHistory
16
+
17
+
18
+ @dataclass
19
+ class AgentResult:
20
+ final_answer: str
21
+ steps: int
22
+ metadata: Dict[str, Any]
23
+
24
+
25
+ @dataclass
26
+ class AgentEvent:
27
+ type: str
28
+ payload: Dict[str, Any]
29
+
30
+
31
+ AgentEventHandler = Callable[[AgentEvent], Awaitable[None]]
32
+
33
+
34
+ class AgentLoopProtocol(Protocol):
35
+ async def run(
36
+ self,
37
+ *,
38
+ model: str,
39
+ messages: List[Dict[str, Any]],
40
+ context: AgentContext,
41
+ selected_tools: Optional[List[str]],
42
+ data_sources: Optional[List[str]],
43
+ max_steps: int,
44
+ temperature: float,
45
+ event_handler: AgentEventHandler,
46
+ ) -> AgentResult: ...