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
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,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: ...
|