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,179 @@
|
|
|
1
|
+
"""Tools mode runner - handles LLM calls with tool execution."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from atlas.domain.messages.models import Message, MessageRole, ToolResult
|
|
7
|
+
from atlas.domain.sessions.models import Session
|
|
8
|
+
from atlas.interfaces.events import EventPublisher
|
|
9
|
+
from atlas.interfaces.llm import LLMProtocol
|
|
10
|
+
from atlas.interfaces.tools import ToolManagerProtocol
|
|
11
|
+
from atlas.modules.prompts.prompt_provider import PromptProvider
|
|
12
|
+
|
|
13
|
+
from ..preprocessors.message_builder import build_session_context
|
|
14
|
+
from ..utilities import error_handler, event_notifier, tool_executor
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Type hint for the update callback
|
|
19
|
+
UpdateCallback = Callable[[Dict[str, Any]], Awaitable[None]]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ToolsModeRunner:
|
|
23
|
+
"""
|
|
24
|
+
Runner for tools mode.
|
|
25
|
+
|
|
26
|
+
Executes LLM calls with tool integration, including tool execution
|
|
27
|
+
and artifact processing.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
llm: LLMProtocol,
|
|
33
|
+
tool_manager: ToolManagerProtocol,
|
|
34
|
+
event_publisher: EventPublisher,
|
|
35
|
+
prompt_provider: Optional[PromptProvider] = None,
|
|
36
|
+
artifact_processor: Optional[Callable[[Session, List[ToolResult], Optional[UpdateCallback]], Awaitable[None]]] = None,
|
|
37
|
+
config_manager=None,
|
|
38
|
+
):
|
|
39
|
+
"""
|
|
40
|
+
Initialize tools mode runner.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
llm: LLM protocol implementation
|
|
44
|
+
tool_manager: Tool manager for tool execution
|
|
45
|
+
event_publisher: Event publisher for UI updates
|
|
46
|
+
prompt_provider: Optional prompt provider
|
|
47
|
+
artifact_processor: Optional callback for processing tool artifacts
|
|
48
|
+
config_manager: Optional config manager for approval settings
|
|
49
|
+
"""
|
|
50
|
+
self.llm = llm
|
|
51
|
+
self.tool_manager = tool_manager
|
|
52
|
+
self.event_publisher = event_publisher
|
|
53
|
+
self.prompt_provider = prompt_provider
|
|
54
|
+
self.artifact_processor = artifact_processor
|
|
55
|
+
self.config_manager = config_manager
|
|
56
|
+
self.skip_approval = False
|
|
57
|
+
|
|
58
|
+
# Verify event_publisher has send_json for elicitation support
|
|
59
|
+
if hasattr(event_publisher, 'send_json'):
|
|
60
|
+
logger.debug(f"ToolsModeRunner initialized with event_publisher that has send_json: {type(event_publisher)}")
|
|
61
|
+
else:
|
|
62
|
+
logger.warning(f"ToolsModeRunner initialized with event_publisher WITHOUT send_json: {type(event_publisher)}")
|
|
63
|
+
|
|
64
|
+
async def run(
|
|
65
|
+
self,
|
|
66
|
+
session: Session,
|
|
67
|
+
model: str,
|
|
68
|
+
messages: List[Dict[str, Any]],
|
|
69
|
+
selected_tools: List[str],
|
|
70
|
+
selected_data_sources: Optional[List[str]] = None,
|
|
71
|
+
user_email: Optional[str] = None,
|
|
72
|
+
tool_choice_required: bool = False,
|
|
73
|
+
update_callback: Optional[UpdateCallback] = None,
|
|
74
|
+
temperature: float = 0.7,
|
|
75
|
+
) -> Dict[str, Any]:
|
|
76
|
+
"""
|
|
77
|
+
Execute tools mode.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
session: Current chat session
|
|
81
|
+
model: LLM model to use
|
|
82
|
+
messages: Message history
|
|
83
|
+
selected_tools: List of tools to make available
|
|
84
|
+
selected_data_sources: Optional list of data sources (for RAG+tools)
|
|
85
|
+
user_email: Optional user email for authorization
|
|
86
|
+
tool_choice_required: Whether tool use is required
|
|
87
|
+
update_callback: Optional callback for streaming updates
|
|
88
|
+
temperature: LLM temperature parameter
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Response dictionary
|
|
92
|
+
"""
|
|
93
|
+
# Resolve tool schemas
|
|
94
|
+
tools_schema = await error_handler.safe_get_tools_schema(self.tool_manager, selected_tools)
|
|
95
|
+
|
|
96
|
+
# Call LLM with tools (and RAG if provided)
|
|
97
|
+
llm_response = await error_handler.safe_call_llm_with_tools(
|
|
98
|
+
llm_caller=self.llm,
|
|
99
|
+
model=model,
|
|
100
|
+
messages=messages,
|
|
101
|
+
tools_schema=tools_schema,
|
|
102
|
+
data_sources=selected_data_sources,
|
|
103
|
+
user_email=user_email,
|
|
104
|
+
tool_choice=("required" if tool_choice_required else "auto"),
|
|
105
|
+
temperature=temperature,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# No tool calls -> treat as plain content
|
|
109
|
+
if not llm_response or not llm_response.has_tool_calls():
|
|
110
|
+
content = llm_response.content if llm_response else ""
|
|
111
|
+
assistant_message = Message(role=MessageRole.ASSISTANT, content=content)
|
|
112
|
+
session.history.add_message(assistant_message)
|
|
113
|
+
|
|
114
|
+
await self.event_publisher.publish_chat_response(
|
|
115
|
+
message=content,
|
|
116
|
+
has_pending_tools=False,
|
|
117
|
+
)
|
|
118
|
+
await self.event_publisher.publish_response_complete()
|
|
119
|
+
|
|
120
|
+
return event_notifier.create_chat_response(content)
|
|
121
|
+
|
|
122
|
+
# Execute tool workflow
|
|
123
|
+
session_context = build_session_context(session)
|
|
124
|
+
|
|
125
|
+
# Ensure update_callback is never None (critical for elicitation)
|
|
126
|
+
effective_callback = update_callback
|
|
127
|
+
if effective_callback is None:
|
|
128
|
+
effective_callback = self._get_send_json()
|
|
129
|
+
logger.debug("Tools mode: update_callback was None, using event_publisher.send_json fallback")
|
|
130
|
+
|
|
131
|
+
if effective_callback is None:
|
|
132
|
+
logger.warning("Tools mode: No update callback available - elicitation will not work!")
|
|
133
|
+
|
|
134
|
+
final_response, tool_results = await tool_executor.execute_tools_workflow(
|
|
135
|
+
llm_response=llm_response,
|
|
136
|
+
messages=messages,
|
|
137
|
+
model=model,
|
|
138
|
+
session_context=session_context,
|
|
139
|
+
tool_manager=self.tool_manager,
|
|
140
|
+
llm_caller=self.llm,
|
|
141
|
+
prompt_provider=self.prompt_provider,
|
|
142
|
+
update_callback=effective_callback,
|
|
143
|
+
config_manager=self.config_manager,
|
|
144
|
+
skip_approval=self.skip_approval,
|
|
145
|
+
user_email=user_email,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Process artifacts if handler provided
|
|
149
|
+
if self.artifact_processor:
|
|
150
|
+
await self.artifact_processor(session, tool_results, effective_callback)
|
|
151
|
+
|
|
152
|
+
# Add final assistant message to history
|
|
153
|
+
assistant_message = Message(
|
|
154
|
+
role=MessageRole.ASSISTANT,
|
|
155
|
+
content=final_response,
|
|
156
|
+
metadata={
|
|
157
|
+
"tools": selected_tools,
|
|
158
|
+
**({"data_sources": selected_data_sources} if selected_data_sources else {}),
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
session.history.add_message(assistant_message)
|
|
162
|
+
|
|
163
|
+
# Emit final chat response
|
|
164
|
+
await self.event_publisher.publish_chat_response(
|
|
165
|
+
message=final_response,
|
|
166
|
+
has_pending_tools=False,
|
|
167
|
+
)
|
|
168
|
+
await self.event_publisher.publish_response_complete()
|
|
169
|
+
|
|
170
|
+
return event_notifier.create_chat_response(final_response)
|
|
171
|
+
|
|
172
|
+
def _get_send_json(self) -> Optional[UpdateCallback]:
|
|
173
|
+
"""Get send_json callback from event publisher if available."""
|
|
174
|
+
if hasattr(self.event_publisher, 'send_json'):
|
|
175
|
+
callback = self.event_publisher.send_json
|
|
176
|
+
logger.debug(f"_get_send_json: event_publisher.send_json = {callback is not None}")
|
|
177
|
+
return callback
|
|
178
|
+
logger.warning(f"_get_send_json: event_publisher does not have send_json method. Type: {type(self.event_publisher)}")
|
|
179
|
+
return None
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Chat orchestrator - coordinates the full chat request flow."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
from atlas.domain.errors import SessionNotFoundError
|
|
8
|
+
from atlas.domain.messages.models import Message, MessageRole
|
|
9
|
+
from atlas.interfaces.events import EventPublisher
|
|
10
|
+
from atlas.interfaces.llm import LLMProtocol
|
|
11
|
+
from atlas.interfaces.sessions import SessionRepository
|
|
12
|
+
from atlas.interfaces.tools import ToolManagerProtocol
|
|
13
|
+
from atlas.modules.prompts.prompt_provider import PromptProvider
|
|
14
|
+
|
|
15
|
+
from .modes.agent import AgentModeRunner
|
|
16
|
+
from .modes.plain import PlainModeRunner
|
|
17
|
+
from .modes.rag import RagModeRunner
|
|
18
|
+
from .modes.tools import ToolsModeRunner
|
|
19
|
+
from .policies.tool_authorization import ToolAuthorizationService
|
|
20
|
+
from .preprocessors.message_builder import MessageBuilder
|
|
21
|
+
from .preprocessors.prompt_override_service import PromptOverrideService
|
|
22
|
+
from .utilities import file_processor
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ChatOrchestrator:
|
|
28
|
+
"""
|
|
29
|
+
Orchestrates the full chat request flow.
|
|
30
|
+
|
|
31
|
+
Coordinates preprocessing, policy checks, mode selection, and execution.
|
|
32
|
+
Provides clean separation between request handling and business logic.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
llm: LLMProtocol,
|
|
38
|
+
event_publisher: EventPublisher,
|
|
39
|
+
session_repository: SessionRepository,
|
|
40
|
+
tool_manager: Optional[ToolManagerProtocol] = None,
|
|
41
|
+
prompt_provider: Optional[PromptProvider] = None,
|
|
42
|
+
file_manager: Optional[Any] = None,
|
|
43
|
+
artifact_processor: Optional[Any] = None,
|
|
44
|
+
plain_mode: Optional[PlainModeRunner] = None,
|
|
45
|
+
rag_mode: Optional[RagModeRunner] = None,
|
|
46
|
+
tools_mode: Optional[ToolsModeRunner] = None,
|
|
47
|
+
agent_mode: Optional[AgentModeRunner] = None,
|
|
48
|
+
):
|
|
49
|
+
"""
|
|
50
|
+
Initialize chat orchestrator.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
llm: LLM protocol implementation
|
|
54
|
+
event_publisher: Event publisher for UI updates
|
|
55
|
+
session_repository: Session storage repository
|
|
56
|
+
tool_manager: Optional tool manager
|
|
57
|
+
prompt_provider: Optional prompt provider
|
|
58
|
+
file_manager: Optional file manager
|
|
59
|
+
artifact_processor: Optional artifact processor callback
|
|
60
|
+
plain_mode: Optional pre-configured plain mode runner
|
|
61
|
+
rag_mode: Optional pre-configured RAG mode runner
|
|
62
|
+
tools_mode: Optional pre-configured tools mode runner
|
|
63
|
+
agent_mode: Optional pre-configured agent mode runner
|
|
64
|
+
"""
|
|
65
|
+
self.llm = llm
|
|
66
|
+
self.event_publisher = event_publisher
|
|
67
|
+
self.session_repository = session_repository
|
|
68
|
+
self.tool_manager = tool_manager
|
|
69
|
+
self.prompt_provider = prompt_provider
|
|
70
|
+
self.file_manager = file_manager
|
|
71
|
+
|
|
72
|
+
# Initialize services
|
|
73
|
+
self.tool_authorization = ToolAuthorizationService(tool_manager=tool_manager)
|
|
74
|
+
self.prompt_override = PromptOverrideService(tool_manager=tool_manager)
|
|
75
|
+
self.message_builder = MessageBuilder(prompt_provider=prompt_provider)
|
|
76
|
+
|
|
77
|
+
# Initialize or use provided mode runners
|
|
78
|
+
self.plain_mode = plain_mode or PlainModeRunner(
|
|
79
|
+
llm=llm,
|
|
80
|
+
event_publisher=event_publisher,
|
|
81
|
+
)
|
|
82
|
+
self.rag_mode = rag_mode or RagModeRunner(
|
|
83
|
+
llm=llm,
|
|
84
|
+
event_publisher=event_publisher,
|
|
85
|
+
)
|
|
86
|
+
self.tools_mode = tools_mode or ToolsModeRunner(
|
|
87
|
+
llm=llm,
|
|
88
|
+
tool_manager=tool_manager,
|
|
89
|
+
event_publisher=event_publisher,
|
|
90
|
+
prompt_provider=prompt_provider,
|
|
91
|
+
artifact_processor=artifact_processor,
|
|
92
|
+
)
|
|
93
|
+
self.agent_mode = agent_mode
|
|
94
|
+
|
|
95
|
+
async def execute(
|
|
96
|
+
self,
|
|
97
|
+
session_id: UUID,
|
|
98
|
+
content: str,
|
|
99
|
+
model: str,
|
|
100
|
+
user_email: Optional[str] = None,
|
|
101
|
+
selected_tools: Optional[List[str]] = None,
|
|
102
|
+
selected_prompts: Optional[List[str]] = None,
|
|
103
|
+
selected_data_sources: Optional[List[str]] = None,
|
|
104
|
+
only_rag: bool = False,
|
|
105
|
+
tool_choice_required: bool = False,
|
|
106
|
+
agent_mode: bool = False,
|
|
107
|
+
temperature: float = 0.7,
|
|
108
|
+
files: Optional[Dict[str, Any]] = None,
|
|
109
|
+
**kwargs
|
|
110
|
+
) -> Dict[str, Any]:
|
|
111
|
+
"""
|
|
112
|
+
Execute a chat request through the full pipeline.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
session_id: Session identifier
|
|
116
|
+
content: User message content
|
|
117
|
+
model: LLM model to use
|
|
118
|
+
user_email: Optional user email
|
|
119
|
+
selected_tools: Optional list of tools
|
|
120
|
+
selected_prompts: Optional list of MCP prompts
|
|
121
|
+
selected_data_sources: Optional list of data sources
|
|
122
|
+
only_rag: Whether to use only RAG (no tools)
|
|
123
|
+
tool_choice_required: Whether tool use is required
|
|
124
|
+
agent_mode: Whether to use agent mode
|
|
125
|
+
temperature: LLM temperature
|
|
126
|
+
files: Optional files to attach
|
|
127
|
+
**kwargs: Additional parameters
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Response dictionary
|
|
131
|
+
"""
|
|
132
|
+
# Get session from repository
|
|
133
|
+
session = await self.session_repository.get(session_id)
|
|
134
|
+
if not session:
|
|
135
|
+
raise SessionNotFoundError(f"Session {session_id} not found")
|
|
136
|
+
|
|
137
|
+
# Add user message to history
|
|
138
|
+
user_message = Message(
|
|
139
|
+
role=MessageRole.USER,
|
|
140
|
+
content=content,
|
|
141
|
+
metadata={"model": model}
|
|
142
|
+
)
|
|
143
|
+
session.history.add_message(user_message)
|
|
144
|
+
session.update_timestamp()
|
|
145
|
+
|
|
146
|
+
# Handle file ingestion
|
|
147
|
+
update_callback = kwargs.get("update_callback")
|
|
148
|
+
logger.debug(f"Orchestrator.execute: update_callback present = {update_callback is not None}")
|
|
149
|
+
session.context = await file_processor.handle_session_files(
|
|
150
|
+
session_context=session.context,
|
|
151
|
+
user_email=user_email,
|
|
152
|
+
files_map=files,
|
|
153
|
+
file_manager=self.file_manager,
|
|
154
|
+
update_callback=update_callback
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Build messages with history and files manifest
|
|
158
|
+
messages = await self.message_builder.build_messages(
|
|
159
|
+
session=session,
|
|
160
|
+
include_files_manifest=True
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Apply MCP prompt override
|
|
164
|
+
messages = await self.prompt_override.apply_prompt_override(
|
|
165
|
+
messages=messages,
|
|
166
|
+
selected_prompts=selected_prompts
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Route to appropriate mode
|
|
170
|
+
if agent_mode and self.agent_mode:
|
|
171
|
+
return await self.agent_mode.run(
|
|
172
|
+
session=session,
|
|
173
|
+
model=model,
|
|
174
|
+
messages=messages,
|
|
175
|
+
selected_tools=selected_tools,
|
|
176
|
+
selected_data_sources=selected_data_sources,
|
|
177
|
+
max_steps=kwargs.get("agent_max_steps", 30),
|
|
178
|
+
temperature=temperature,
|
|
179
|
+
agent_loop_strategy=kwargs.get("agent_loop_strategy"),
|
|
180
|
+
)
|
|
181
|
+
elif selected_tools and not only_rag:
|
|
182
|
+
# Apply tool authorization
|
|
183
|
+
selected_tools = await self.tool_authorization.filter_authorized_tools(
|
|
184
|
+
selected_tools=selected_tools,
|
|
185
|
+
user_email=user_email
|
|
186
|
+
)
|
|
187
|
+
return await self.tools_mode.run(
|
|
188
|
+
session=session,
|
|
189
|
+
model=model,
|
|
190
|
+
messages=messages,
|
|
191
|
+
selected_tools=selected_tools,
|
|
192
|
+
selected_data_sources=selected_data_sources,
|
|
193
|
+
user_email=user_email,
|
|
194
|
+
tool_choice_required=tool_choice_required,
|
|
195
|
+
update_callback=update_callback,
|
|
196
|
+
temperature=temperature,
|
|
197
|
+
)
|
|
198
|
+
elif selected_data_sources:
|
|
199
|
+
return await self.rag_mode.run(
|
|
200
|
+
session=session,
|
|
201
|
+
model=model,
|
|
202
|
+
messages=messages,
|
|
203
|
+
data_sources=selected_data_sources,
|
|
204
|
+
user_email=user_email,
|
|
205
|
+
temperature=temperature,
|
|
206
|
+
)
|
|
207
|
+
else:
|
|
208
|
+
return await self.plain_mode.run(
|
|
209
|
+
session=session,
|
|
210
|
+
model=model,
|
|
211
|
+
messages=messages,
|
|
212
|
+
temperature=temperature,
|
|
213
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Policy modules for chat application."""
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Tool authorization policy - filters tools based on user access control."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, List, Optional
|
|
5
|
+
|
|
6
|
+
from atlas.core.auth import is_user_in_group
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ToolAuthorizationService:
|
|
12
|
+
"""
|
|
13
|
+
Service that filters selected tools based on user authorization.
|
|
14
|
+
|
|
15
|
+
Enforces MCP tool access control lists (ACLs) by checking:
|
|
16
|
+
- User authorization to MCP servers
|
|
17
|
+
- Special cases (e.g., canvas_canvas tool is always allowed)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, tool_manager: Optional[Any] = None):
|
|
21
|
+
"""
|
|
22
|
+
Initialize the tool authorization service.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
tool_manager: Optional tool manager with server configuration
|
|
26
|
+
"""
|
|
27
|
+
self.tool_manager = tool_manager
|
|
28
|
+
|
|
29
|
+
async def filter_authorized_tools(
|
|
30
|
+
self,
|
|
31
|
+
selected_tools: List[str],
|
|
32
|
+
user_email: Optional[str] = None,
|
|
33
|
+
) -> List[str]:
|
|
34
|
+
"""
|
|
35
|
+
Filter tools to only those the user is authorized to use.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
selected_tools: List of tool names (format: "server_toolname")
|
|
39
|
+
user_email: Email of the user making the request
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Filtered list of authorized tool names
|
|
43
|
+
"""
|
|
44
|
+
if not selected_tools or not self.tool_manager:
|
|
45
|
+
return selected_tools or []
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
user = user_email or ""
|
|
49
|
+
|
|
50
|
+
# Get authorized servers for this user
|
|
51
|
+
authorized_servers = await self._get_authorized_servers(user)
|
|
52
|
+
|
|
53
|
+
# Filter tools by server prefix
|
|
54
|
+
filtered_tools: List[str] = []
|
|
55
|
+
for tool in selected_tools:
|
|
56
|
+
# Special case: canvas_canvas is always allowed
|
|
57
|
+
if tool == "canvas_canvas":
|
|
58
|
+
filtered_tools.append(tool)
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
# Check if tool belongs to an authorized server
|
|
62
|
+
if isinstance(tool, str) and "_" in tool:
|
|
63
|
+
# Match against authorized servers by checking if tool name starts with server_
|
|
64
|
+
# This handles server names that contain underscores (e.g., "pptx_generator")
|
|
65
|
+
matched_server = None
|
|
66
|
+
for auth_server in authorized_servers:
|
|
67
|
+
if tool.startswith(f"{auth_server}_"):
|
|
68
|
+
matched_server = auth_server
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
if matched_server:
|
|
72
|
+
filtered_tools.append(tool)
|
|
73
|
+
|
|
74
|
+
return filtered_tools
|
|
75
|
+
|
|
76
|
+
except Exception:
|
|
77
|
+
logger.debug(
|
|
78
|
+
"Tool ACL filtering failed; proceeding with original selection",
|
|
79
|
+
exc_info=True
|
|
80
|
+
)
|
|
81
|
+
return selected_tools
|
|
82
|
+
|
|
83
|
+
async def _get_authorized_servers(self, user: str) -> List[str]:
|
|
84
|
+
"""
|
|
85
|
+
Get list of MCP servers the user is authorized to access.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
user: User email
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
List of authorized server names
|
|
92
|
+
"""
|
|
93
|
+
# Use tool_manager's authorization method if available
|
|
94
|
+
if hasattr(self.tool_manager, "get_authorized_servers"):
|
|
95
|
+
return await self.tool_manager.get_authorized_servers(user, is_user_in_group) # type: ignore[attr-defined]
|
|
96
|
+
|
|
97
|
+
# If no authorization method available, return empty list (no authorized servers)
|
|
98
|
+
logger.warning("Tool manager has no get_authorized_servers method for user %s", user)
|
|
99
|
+
return []
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Preprocessor modules for chat application."""
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Message builder - constructs messages with history and files manifest."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from atlas.domain.sessions.models import Session
|
|
7
|
+
from atlas.modules.prompts.prompt_provider import PromptProvider
|
|
8
|
+
|
|
9
|
+
from ..utilities import file_processor
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_session_context(session: Session) -> Dict[str, Any]:
|
|
15
|
+
"""
|
|
16
|
+
Build session context dictionary from session.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
session: Chat session
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Session context dictionary
|
|
23
|
+
"""
|
|
24
|
+
return {
|
|
25
|
+
"session_id": session.id,
|
|
26
|
+
"user_email": session.user_email,
|
|
27
|
+
"files": session.context.get("files", {}),
|
|
28
|
+
**session.context
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MessageBuilder:
|
|
33
|
+
"""
|
|
34
|
+
Service that builds complete message arrays for LLM calls.
|
|
35
|
+
|
|
36
|
+
Combines conversation history with files manifest and system prompt.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, prompt_provider: Optional[PromptProvider] = None):
|
|
40
|
+
"""
|
|
41
|
+
Initialize message builder.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
prompt_provider: Optional prompt provider for loading system prompt
|
|
45
|
+
"""
|
|
46
|
+
self.prompt_provider = prompt_provider
|
|
47
|
+
|
|
48
|
+
async def build_messages(
|
|
49
|
+
self,
|
|
50
|
+
session: Session,
|
|
51
|
+
include_files_manifest: bool = True,
|
|
52
|
+
include_system_prompt: bool = True,
|
|
53
|
+
) -> List[Dict[str, Any]]:
|
|
54
|
+
"""
|
|
55
|
+
Build messages array from session history and context.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
session: Current chat session
|
|
59
|
+
include_files_manifest: Whether to append files manifest
|
|
60
|
+
include_system_prompt: Whether to prepend system prompt
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
List of messages ready for LLM call
|
|
64
|
+
"""
|
|
65
|
+
messages = []
|
|
66
|
+
|
|
67
|
+
# Optionally add system prompt at the beginning
|
|
68
|
+
if include_system_prompt and self.prompt_provider:
|
|
69
|
+
system_prompt = self.prompt_provider.get_system_prompt(
|
|
70
|
+
user_email=session.user_email
|
|
71
|
+
)
|
|
72
|
+
if system_prompt:
|
|
73
|
+
messages.append({"role": "system", "content": system_prompt})
|
|
74
|
+
logger.debug(f"Added system prompt (len={len(system_prompt)})")
|
|
75
|
+
|
|
76
|
+
# Get conversation history from session
|
|
77
|
+
history_messages = session.history.get_messages_for_llm()
|
|
78
|
+
messages.extend(history_messages)
|
|
79
|
+
|
|
80
|
+
# Optionally add files manifest
|
|
81
|
+
if include_files_manifest:
|
|
82
|
+
session_context = build_session_context(session)
|
|
83
|
+
files_in_context = session_context.get("files", {})
|
|
84
|
+
logger.debug(f"Session has {len(files_in_context)} files: {list(files_in_context.keys())}")
|
|
85
|
+
files_manifest = file_processor.build_files_manifest(session_context)
|
|
86
|
+
if files_manifest:
|
|
87
|
+
logger.debug(f"Adding files manifest to messages: {files_manifest['content'][:100]}")
|
|
88
|
+
messages.append(files_manifest)
|
|
89
|
+
else:
|
|
90
|
+
logger.warning("No files manifest generated despite include_files_manifest=True")
|
|
91
|
+
|
|
92
|
+
return messages
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Prompt override service - handles MCP system prompt injection."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PromptOverrideService:
|
|
10
|
+
"""
|
|
11
|
+
Service that handles MCP prompt override injection.
|
|
12
|
+
|
|
13
|
+
Retrieves MCP-provided prompts and injects them as system messages,
|
|
14
|
+
applying only the first valid prompt found.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, tool_manager: Optional[Any] = None):
|
|
18
|
+
"""
|
|
19
|
+
Initialize the prompt override service.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
tool_manager: Optional tool manager with prompt retrieval capability
|
|
23
|
+
"""
|
|
24
|
+
self.tool_manager = tool_manager
|
|
25
|
+
|
|
26
|
+
async def apply_prompt_override(
|
|
27
|
+
self,
|
|
28
|
+
messages: List[Dict[str, Any]],
|
|
29
|
+
selected_prompts: Optional[List[str]] = None,
|
|
30
|
+
) -> List[Dict[str, Any]]:
|
|
31
|
+
"""
|
|
32
|
+
Apply MCP prompt override if selected prompts are provided.
|
|
33
|
+
|
|
34
|
+
Only the first valid prompt is applied, prepended as a system message.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
messages: Current message history
|
|
38
|
+
selected_prompts: List of prompt keys (format: "server_promptname")
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Messages with prompt override prepended (if applicable)
|
|
42
|
+
"""
|
|
43
|
+
if not selected_prompts or not self.tool_manager:
|
|
44
|
+
return messages
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
# Iterate in order; when found, fetch prompt content and inject
|
|
48
|
+
for key in selected_prompts:
|
|
49
|
+
if not isinstance(key, str) or "_" not in key:
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
server, prompt_name = key.split("_", 1)
|
|
53
|
+
|
|
54
|
+
# Retrieve prompt from MCP
|
|
55
|
+
try:
|
|
56
|
+
prompt_obj = await self.tool_manager.get_prompt(server, prompt_name)
|
|
57
|
+
prompt_text = self._extract_prompt_text(prompt_obj)
|
|
58
|
+
|
|
59
|
+
if prompt_text:
|
|
60
|
+
# Prepend as system message override
|
|
61
|
+
messages = [{"role": "system", "content": prompt_text}] + messages
|
|
62
|
+
logger.info(
|
|
63
|
+
"Applied MCP system prompt override (len=%d)",
|
|
64
|
+
len(prompt_text),
|
|
65
|
+
)
|
|
66
|
+
break # apply only one
|
|
67
|
+
|
|
68
|
+
except Exception:
|
|
69
|
+
logger.debug("Failed retrieving MCP prompt %s", key, exc_info=True)
|
|
70
|
+
|
|
71
|
+
except Exception:
|
|
72
|
+
logger.debug(
|
|
73
|
+
"Prompt override injection skipped due to non-fatal error",
|
|
74
|
+
exc_info=True
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return messages
|
|
78
|
+
|
|
79
|
+
def _extract_prompt_text(self, prompt_obj: Any) -> Optional[str]:
|
|
80
|
+
"""
|
|
81
|
+
Extract text content from various MCP prompt object formats.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
prompt_obj: Prompt object from MCP (could be string or structured object)
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
Extracted prompt text, or None if extraction failed
|
|
88
|
+
"""
|
|
89
|
+
# Simple string case
|
|
90
|
+
if isinstance(prompt_obj, str):
|
|
91
|
+
return prompt_obj
|
|
92
|
+
|
|
93
|
+
# FastMCP PromptMessage-like: may have 'content' list with text entries
|
|
94
|
+
if hasattr(prompt_obj, "content"):
|
|
95
|
+
content_field = getattr(prompt_obj, "content")
|
|
96
|
+
|
|
97
|
+
# content could be list of objects with 'text'
|
|
98
|
+
if isinstance(content_field, list) and content_field:
|
|
99
|
+
first = content_field[0]
|
|
100
|
+
if hasattr(first, "text") and isinstance(first.text, str):
|
|
101
|
+
return first.text
|
|
102
|
+
|
|
103
|
+
# Fallback: string dump
|
|
104
|
+
return str(prompt_obj)
|