autobyteus 1.1.2__py3-none-any.whl → 1.1.4__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.
- autobyteus/agent/agent.py +1 -1
- autobyteus/agent/bootstrap_steps/__init__.py +2 -0
- autobyteus/agent/bootstrap_steps/agent_bootstrapper.py +2 -0
- autobyteus/agent/bootstrap_steps/mcp_server_prewarming_step.py +71 -0
- autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +4 -2
- autobyteus/agent/context/agent_config.py +36 -5
- autobyteus/agent/events/worker_event_dispatcher.py +1 -2
- autobyteus/agent/handlers/inter_agent_message_event_handler.py +1 -1
- autobyteus/agent/handlers/llm_user_message_ready_event_handler.py +2 -2
- autobyteus/agent/handlers/tool_result_event_handler.py +48 -20
- autobyteus/agent/handlers/user_input_message_event_handler.py +1 -1
- autobyteus/agent/input_processor/__init__.py +1 -7
- autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +41 -12
- autobyteus/agent/message/context_file_type.py +6 -0
- autobyteus/agent/message/send_message_to.py +68 -99
- autobyteus/agent/phases/discover.py +2 -1
- autobyteus/agent/runtime/agent_worker.py +25 -34
- autobyteus/agent/shutdown_steps/__init__.py +17 -0
- autobyteus/agent/shutdown_steps/agent_shutdown_orchestrator.py +63 -0
- autobyteus/agent/shutdown_steps/base_shutdown_step.py +33 -0
- autobyteus/agent/shutdown_steps/llm_instance_cleanup_step.py +45 -0
- autobyteus/agent/shutdown_steps/mcp_server_cleanup_step.py +32 -0
- autobyteus/agent/tool_execution_result_processor/__init__.py +9 -0
- autobyteus/agent/tool_execution_result_processor/base_processor.py +46 -0
- autobyteus/agent/tool_execution_result_processor/processor_definition.py +36 -0
- autobyteus/agent/tool_execution_result_processor/processor_meta.py +36 -0
- autobyteus/agent/tool_execution_result_processor/processor_registry.py +70 -0
- autobyteus/agent/workspace/base_workspace.py +17 -2
- autobyteus/cli/__init__.py +1 -1
- autobyteus/cli/cli_display.py +1 -1
- autobyteus/cli/workflow_tui/__init__.py +4 -0
- autobyteus/cli/workflow_tui/app.py +210 -0
- autobyteus/cli/workflow_tui/state.py +189 -0
- autobyteus/cli/workflow_tui/widgets/__init__.py +6 -0
- autobyteus/cli/workflow_tui/widgets/agent_list_sidebar.py +149 -0
- autobyteus/cli/workflow_tui/widgets/focus_pane.py +335 -0
- autobyteus/cli/workflow_tui/widgets/logo.py +27 -0
- autobyteus/cli/workflow_tui/widgets/renderables.py +70 -0
- autobyteus/cli/workflow_tui/widgets/shared.py +51 -0
- autobyteus/cli/workflow_tui/widgets/status_bar.py +14 -0
- autobyteus/events/event_types.py +3 -0
- autobyteus/llm/api/lmstudio_llm.py +37 -0
- autobyteus/llm/api/openai_compatible_llm.py +20 -3
- autobyteus/llm/llm_factory.py +2 -0
- autobyteus/llm/lmstudio_provider.py +89 -0
- autobyteus/llm/providers.py +1 -0
- autobyteus/llm/token_counter/token_counter_factory.py +2 -0
- autobyteus/tools/__init__.py +2 -0
- autobyteus/tools/ask_user_input.py +2 -1
- autobyteus/tools/base_tool.py +2 -0
- autobyteus/tools/bash/bash_executor.py +2 -1
- autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +2 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +3 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +3 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +3 -0
- autobyteus/tools/browser/standalone/google_search_ui.py +2 -0
- autobyteus/tools/browser/standalone/navigate_to.py +2 -0
- autobyteus/tools/browser/standalone/web_page_pdf_generator.py +3 -0
- autobyteus/tools/browser/standalone/webpage_image_downloader.py +3 -0
- autobyteus/tools/browser/standalone/webpage_reader.py +2 -0
- autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +3 -0
- autobyteus/tools/file/file_reader.py +36 -9
- autobyteus/tools/file/file_writer.py +37 -9
- autobyteus/tools/functional_tool.py +5 -4
- autobyteus/tools/image_downloader.py +2 -0
- autobyteus/tools/mcp/__init__.py +10 -7
- autobyteus/tools/mcp/call_handlers/__init__.py +0 -2
- autobyteus/tools/mcp/config_service.py +1 -6
- autobyteus/tools/mcp/factory.py +12 -26
- autobyteus/tools/mcp/server/__init__.py +16 -0
- autobyteus/tools/mcp/server/base_managed_mcp_server.py +139 -0
- autobyteus/tools/mcp/server/http_managed_mcp_server.py +29 -0
- autobyteus/tools/mcp/server/proxy.py +36 -0
- autobyteus/tools/mcp/server/stdio_managed_mcp_server.py +33 -0
- autobyteus/tools/mcp/server_instance_manager.py +93 -0
- autobyteus/tools/mcp/tool.py +28 -46
- autobyteus/tools/mcp/tool_registrar.py +179 -0
- autobyteus/tools/mcp/types.py +10 -21
- autobyteus/tools/pdf_downloader.py +2 -1
- autobyteus/tools/registry/tool_definition.py +20 -7
- autobyteus/tools/registry/tool_registry.py +75 -28
- autobyteus/tools/timer.py +2 -0
- autobyteus/tools/tool_category.py +14 -4
- autobyteus/tools/tool_meta.py +6 -1
- autobyteus/tools/tool_origin.py +10 -0
- autobyteus/workflow/agentic_workflow.py +93 -0
- autobyteus/{agent/workflow → workflow}/base_agentic_workflow.py +19 -27
- autobyteus/workflow/bootstrap_steps/__init__.py +20 -0
- autobyteus/workflow/bootstrap_steps/agent_tool_injection_step.py +34 -0
- autobyteus/workflow/bootstrap_steps/base_workflow_bootstrap_step.py +23 -0
- autobyteus/workflow/bootstrap_steps/coordinator_initialization_step.py +41 -0
- autobyteus/workflow/bootstrap_steps/coordinator_prompt_preparation_step.py +108 -0
- autobyteus/workflow/bootstrap_steps/workflow_bootstrapper.py +50 -0
- autobyteus/workflow/bootstrap_steps/workflow_runtime_queue_initialization_step.py +25 -0
- autobyteus/workflow/context/__init__.py +17 -0
- autobyteus/workflow/context/team_manager.py +147 -0
- autobyteus/workflow/context/workflow_config.py +30 -0
- autobyteus/workflow/context/workflow_context.py +61 -0
- autobyteus/workflow/context/workflow_node_config.py +76 -0
- autobyteus/workflow/context/workflow_runtime_state.py +53 -0
- autobyteus/workflow/events/__init__.py +29 -0
- autobyteus/workflow/events/workflow_event_dispatcher.py +39 -0
- autobyteus/workflow/events/workflow_events.py +53 -0
- autobyteus/workflow/events/workflow_input_event_queue_manager.py +21 -0
- autobyteus/workflow/exceptions.py +8 -0
- autobyteus/workflow/factory/__init__.py +9 -0
- autobyteus/workflow/factory/workflow_factory.py +99 -0
- autobyteus/workflow/handlers/__init__.py +19 -0
- autobyteus/workflow/handlers/base_workflow_event_handler.py +16 -0
- autobyteus/workflow/handlers/inter_agent_message_request_event_handler.py +61 -0
- autobyteus/workflow/handlers/lifecycle_workflow_event_handler.py +27 -0
- autobyteus/workflow/handlers/process_user_message_event_handler.py +46 -0
- autobyteus/workflow/handlers/tool_approval_workflow_event_handler.py +39 -0
- autobyteus/workflow/handlers/workflow_event_handler_registry.py +23 -0
- autobyteus/workflow/phases/__init__.py +11 -0
- autobyteus/workflow/phases/workflow_operational_phase.py +19 -0
- autobyteus/workflow/phases/workflow_phase_manager.py +48 -0
- autobyteus/workflow/runtime/__init__.py +13 -0
- autobyteus/workflow/runtime/workflow_runtime.py +82 -0
- autobyteus/workflow/runtime/workflow_worker.py +117 -0
- autobyteus/workflow/shutdown_steps/__init__.py +17 -0
- autobyteus/workflow/shutdown_steps/agent_team_shutdown_step.py +42 -0
- autobyteus/workflow/shutdown_steps/base_workflow_shutdown_step.py +16 -0
- autobyteus/workflow/shutdown_steps/bridge_cleanup_step.py +28 -0
- autobyteus/workflow/shutdown_steps/sub_workflow_shutdown_step.py +41 -0
- autobyteus/workflow/shutdown_steps/workflow_shutdown_orchestrator.py +35 -0
- autobyteus/workflow/streaming/__init__.py +26 -0
- autobyteus/workflow/streaming/agent_event_bridge.py +48 -0
- autobyteus/workflow/streaming/agent_event_multiplexer.py +70 -0
- autobyteus/workflow/streaming/workflow_event_bridge.py +50 -0
- autobyteus/workflow/streaming/workflow_event_notifier.py +83 -0
- autobyteus/workflow/streaming/workflow_event_stream.py +33 -0
- autobyteus/workflow/streaming/workflow_stream_event_payloads.py +28 -0
- autobyteus/workflow/streaming/workflow_stream_events.py +45 -0
- autobyteus/workflow/utils/__init__.py +9 -0
- autobyteus/workflow/utils/wait_for_idle.py +46 -0
- autobyteus/workflow/workflow_builder.py +151 -0
- {autobyteus-1.1.2.dist-info → autobyteus-1.1.4.dist-info}/METADATA +16 -13
- {autobyteus-1.1.2.dist-info → autobyteus-1.1.4.dist-info}/RECORD +156 -75
- {autobyteus-1.1.2.dist-info → autobyteus-1.1.4.dist-info}/top_level.txt +1 -0
- examples/__init__.py +1 -0
- examples/discover_phase_transitions.py +104 -0
- examples/run_browser_agent.py +260 -0
- examples/run_google_slides_agent.py +286 -0
- examples/run_mcp_browser_client.py +174 -0
- examples/run_mcp_google_slides_client.py +270 -0
- examples/run_mcp_list_tools.py +189 -0
- examples/run_poem_writer.py +274 -0
- examples/run_sqlite_agent.py +293 -0
- examples/workflow/__init__.py +1 -0
- examples/workflow/run_basic_research_workflow.py +189 -0
- examples/workflow/run_code_review_workflow.py +269 -0
- examples/workflow/run_debate_workflow.py +212 -0
- examples/workflow/run_workflow_with_tui.py +153 -0
- autobyteus/agent/context/agent_phase_manager.py +0 -264
- autobyteus/agent/context/phases.py +0 -49
- autobyteus/agent/group/__init__.py +0 -0
- autobyteus/agent/group/agent_group.py +0 -164
- autobyteus/agent/group/agent_group_context.py +0 -81
- autobyteus/agent/input_processor/content_prefixing_input_processor.py +0 -41
- autobyteus/agent/input_processor/metadata_appending_input_processor.py +0 -34
- autobyteus/agent/input_processor/passthrough_input_processor.py +0 -33
- autobyteus/agent/workflow/__init__.py +0 -11
- autobyteus/agent/workflow/agentic_workflow.py +0 -89
- autobyteus/tools/mcp/call_handlers/sse_handler.py +0 -22
- autobyteus/tools/mcp/registrar.py +0 -323
- autobyteus/workflow/simple_task.py +0 -98
- autobyteus/workflow/task.py +0 -147
- autobyteus/workflow/workflow.py +0 -49
- {autobyteus-1.1.2.dist-info → autobyteus-1.1.4.dist-info}/WHEEL +0 -0
- {autobyteus-1.1.2.dist-info → autobyteus-1.1.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# file: autobyteus/examples/run_mcp_browser_client.py
|
|
2
|
+
"""
|
|
3
|
+
This example script demonstrates how to create a standalone MCP client in Python
|
|
4
|
+
to connect to and interact with the Browser MCP server.
|
|
5
|
+
|
|
6
|
+
This script uses only the 'mcp' library and standard Python libraries,
|
|
7
|
+
intentionally avoiding the 'autobyteus' framework abstractions.
|
|
8
|
+
This approach is useful for understanding the low-level communication
|
|
9
|
+
with an MCP server.
|
|
10
|
+
|
|
11
|
+
The script will:
|
|
12
|
+
1. Define the parameters to launch the Browser MCP server (`npx @browsermcp/mcp@latest`).
|
|
13
|
+
2. Start the server process and establish an stdio transport.
|
|
14
|
+
3. Initialize an MCP client session.
|
|
15
|
+
4. List the available tools from the server.
|
|
16
|
+
5. Call the 'open_page' tool to open a website.
|
|
17
|
+
6. Call the 'get_page_content' tool to retrieve the page's text.
|
|
18
|
+
7. Properly clean up the session and server process.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import logging
|
|
23
|
+
import sys
|
|
24
|
+
import json
|
|
25
|
+
from contextlib import AsyncExitStack
|
|
26
|
+
from mcp import ClientSession, StdioServerParameters
|
|
27
|
+
from mcp.client.stdio import stdio_client
|
|
28
|
+
|
|
29
|
+
# --- Logging Setup ---
|
|
30
|
+
logging.basicConfig(
|
|
31
|
+
level=logging.INFO,
|
|
32
|
+
format='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
|
|
33
|
+
stream=sys.stdout,
|
|
34
|
+
)
|
|
35
|
+
logger = logging.getLogger("mcp_browser_client")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MCPBrowserClient:
|
|
39
|
+
"""
|
|
40
|
+
A client for interacting with a Browser MCP server over stdio.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
self.session: ClientSession | None = None
|
|
45
|
+
self.exit_stack = AsyncExitStack()
|
|
46
|
+
self.page_id: str | None = None
|
|
47
|
+
|
|
48
|
+
async def connect(self):
|
|
49
|
+
"""
|
|
50
|
+
Starts the browser MCP server and connects to it.
|
|
51
|
+
"""
|
|
52
|
+
logger.info("Defining server parameters for Browser MCP...")
|
|
53
|
+
# These parameters specify how to run the MCP server.
|
|
54
|
+
# This is the same command used by the `run_browser_agent.py` example.
|
|
55
|
+
server_params = StdioServerParameters(
|
|
56
|
+
command="npx",
|
|
57
|
+
args=["@browsermcp/mcp@latest"],
|
|
58
|
+
env=None,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
logger.info(f"Starting server with command: '{server_params.command} {' '.join(server_params.args)}'")
|
|
62
|
+
|
|
63
|
+
# `stdio_client` is a context manager that starts the process
|
|
64
|
+
# and provides reader/writer streams for communication.
|
|
65
|
+
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
|
|
66
|
+
read, write = stdio_transport
|
|
67
|
+
|
|
68
|
+
logger.info("Server process started. Establishing MCP session...")
|
|
69
|
+
|
|
70
|
+
# `ClientSession` is another context manager that handles the MCP protocol.
|
|
71
|
+
self.session = await self.exit_stack.enter_async_context(ClientSession(read, write))
|
|
72
|
+
|
|
73
|
+
# The initialize handshake must be performed after connection.
|
|
74
|
+
await self.session.initialize()
|
|
75
|
+
logger.info("MCP session initialized successfully.")
|
|
76
|
+
|
|
77
|
+
# Let's see what tools the server offers.
|
|
78
|
+
response = await self.session.list_tools()
|
|
79
|
+
tool_names = [tool.name for tool in response.tools]
|
|
80
|
+
logger.info(f"Connected to server. Available tools: {tool_names}")
|
|
81
|
+
|
|
82
|
+
async def call_tool(self, tool_name: str, **kwargs):
|
|
83
|
+
"""
|
|
84
|
+
A wrapper to call a tool on the server and print the result.
|
|
85
|
+
"""
|
|
86
|
+
if not self.session:
|
|
87
|
+
raise RuntimeError("Client not connected. Call connect() first.")
|
|
88
|
+
|
|
89
|
+
logger.info(f"Calling tool '{tool_name}' with arguments: {kwargs}")
|
|
90
|
+
try:
|
|
91
|
+
result = await self.session.call_tool(tool_name, kwargs)
|
|
92
|
+
logger.info(f"Tool '{tool_name}' executed successfully.")
|
|
93
|
+
|
|
94
|
+
# The result content is a list of blocks. For many tools, it's a single text block.
|
|
95
|
+
if result.content and hasattr(result.content[0], 'text'):
|
|
96
|
+
tool_output = result.content[0].text
|
|
97
|
+
logger.info(f"--> Result from '{tool_name}':\n{tool_output[:500]}...")
|
|
98
|
+
return tool_output
|
|
99
|
+
else:
|
|
100
|
+
logger.info(f"--> Result from '{tool_name}' has no text content: {result.content}")
|
|
101
|
+
return result.content
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error(f"An error occurred while calling tool '{tool_name}': {e}", exc_info=True)
|
|
104
|
+
raise
|
|
105
|
+
|
|
106
|
+
async def cleanup(self):
|
|
107
|
+
"""
|
|
108
|
+
Closes the session and stops the server process.
|
|
109
|
+
The AsyncExitStack handles this automatically.
|
|
110
|
+
"""
|
|
111
|
+
logger.info("Cleaning up resources and closing server connection...")
|
|
112
|
+
await self.exit_stack.aclose()
|
|
113
|
+
logger.info("Cleanup complete.")
|
|
114
|
+
|
|
115
|
+
async def run_demo_flow(self):
|
|
116
|
+
"""
|
|
117
|
+
Executes a simple workflow: open a page, get its content, and close it.
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
# 1. Open a page
|
|
121
|
+
open_page_result = await self.call_tool("open_page", url="https://www.google.com/search?q=mcp+protocol")
|
|
122
|
+
|
|
123
|
+
# The 'open_page' tool returns a JSON string with the pageId.
|
|
124
|
+
# We need to parse it to use in subsequent calls.
|
|
125
|
+
if not isinstance(open_page_result, str):
|
|
126
|
+
logger.error(f"Expected a string from 'open_page', but got {type(open_page_result)}")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
page_info = json.loads(open_page_result)
|
|
131
|
+
self.page_id = page_info.get("pageId")
|
|
132
|
+
if not self.page_id:
|
|
133
|
+
logger.error("Could not find 'pageId' in the result from 'open_page'.")
|
|
134
|
+
return
|
|
135
|
+
logger.info(f"Successfully opened page. Page ID: {self.page_id}")
|
|
136
|
+
except (json.JSONDecodeError, AttributeError) as e:
|
|
137
|
+
logger.error(f"Failed to parse pageId from 'open_page' result: {e}")
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
# 2. Get the page content
|
|
141
|
+
# Add a small delay for the page to potentially load dynamic content.
|
|
142
|
+
logger.info("Waiting for 2 seconds before getting content...")
|
|
143
|
+
await asyncio.sleep(2)
|
|
144
|
+
|
|
145
|
+
await self.call_tool("get_page_content", pageId=self.page_id)
|
|
146
|
+
|
|
147
|
+
# 3. Close the page
|
|
148
|
+
await self.call_tool("close_page", pageId=self.page_id)
|
|
149
|
+
logger.info(f"Page {self.page_id} closed.")
|
|
150
|
+
|
|
151
|
+
except Exception as e:
|
|
152
|
+
logger.error(f"An error occurred during the demo flow: {e}")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def main():
|
|
156
|
+
"""
|
|
157
|
+
Main function to run the MCP Browser Client.
|
|
158
|
+
"""
|
|
159
|
+
logger.info("--- Starting Standalone MCP Browser Client Example ---")
|
|
160
|
+
client = MCPBrowserClient()
|
|
161
|
+
try:
|
|
162
|
+
await client.connect()
|
|
163
|
+
await client.run_demo_flow()
|
|
164
|
+
finally:
|
|
165
|
+
await client.cleanup()
|
|
166
|
+
logger.info("--- Standalone MCP Browser Client Example Finished ---")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == "__main__":
|
|
170
|
+
try:
|
|
171
|
+
asyncio.run(main())
|
|
172
|
+
except (KeyboardInterrupt, SystemExit):
|
|
173
|
+
logger.info("Script interrupted by user. Exiting.")
|
|
174
|
+
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
import argparse
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import AsyncIterator, Optional, List
|
|
10
|
+
|
|
11
|
+
# --- Boilerplate to make the script runnable from the project root ---
|
|
12
|
+
|
|
13
|
+
# Ensure the autobyteus package is discoverable
|
|
14
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
15
|
+
PACKAGE_ROOT = SCRIPT_DIR.parent
|
|
16
|
+
if str(PACKAGE_ROOT) not in sys.path:
|
|
17
|
+
sys.path.insert(0, str(PACKAGE_ROOT))
|
|
18
|
+
|
|
19
|
+
# Load environment variables from .env file in the project root
|
|
20
|
+
try:
|
|
21
|
+
from dotenv import load_dotenv
|
|
22
|
+
env_file_path = PACKAGE_ROOT / ".env"
|
|
23
|
+
if env_file_path.exists():
|
|
24
|
+
load_dotenv(env_file_path)
|
|
25
|
+
print(f"Loaded environment variables from: {env_file_path}")
|
|
26
|
+
else:
|
|
27
|
+
print(f"Info: No .env file found at: {env_file_path}. Relying on exported environment variables.")
|
|
28
|
+
except ImportError:
|
|
29
|
+
print("Warning: python-dotenv not installed. Cannot load .env file.")
|
|
30
|
+
|
|
31
|
+
# --- Imports for the MCP Client Example ---
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
# High-level components for the full workflow
|
|
35
|
+
from autobyteus.tools.mcp import McpToolRegistrar
|
|
36
|
+
from autobyteus.tools.registry import ToolRegistry, default_tool_registry, ToolDefinition
|
|
37
|
+
from autobyteus.agent.context import AgentContext, AgentConfig, AgentRuntimeState
|
|
38
|
+
from autobyteus.llm.base_llm import BaseLLM
|
|
39
|
+
from autobyteus.llm.utils.response_types import CompleteResponse, ChunkResponse
|
|
40
|
+
except ImportError as e:
|
|
41
|
+
print(f"Error importing autobyteus components: {e}", file=sys.stderr)
|
|
42
|
+
print("Please ensure that the autobyteus library is installed and accessible in your PYTHONPATH.", file=sys.stderr)
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
# --- Basic Logging Setup ---
|
|
46
|
+
# A logger for this script
|
|
47
|
+
logger = logging.getLogger("mcp_client_example")
|
|
48
|
+
|
|
49
|
+
# --- Dummy LLM for creating AgentContext ---
|
|
50
|
+
class DummyLLM(BaseLLM):
|
|
51
|
+
"""A dummy LLM implementation required to instantiate AgentConfig."""
|
|
52
|
+
def __init__(self):
|
|
53
|
+
# We need to provide a model and config to the BaseLLM constructor.
|
|
54
|
+
# Let's use a dummy model configuration.
|
|
55
|
+
from autobyteus.llm.models import LLMModel
|
|
56
|
+
from autobyteus.llm.utils.llm_config import LLMConfig
|
|
57
|
+
from autobyteus.llm.llm_factory import default_llm_factory
|
|
58
|
+
|
|
59
|
+
# Ensure factory is initialized to access models
|
|
60
|
+
default_llm_factory.ensure_initialized()
|
|
61
|
+
|
|
62
|
+
# Pick any existing model for the dummy, e.g., the first one available.
|
|
63
|
+
try:
|
|
64
|
+
# Iterating through LLMModel is now possible due to metaclass
|
|
65
|
+
dummy_model_instance = next(iter(LLMModel))
|
|
66
|
+
except StopIteration:
|
|
67
|
+
# This is a fallback in case no models are registered, which is unlikely but safe.
|
|
68
|
+
raise RuntimeError("No LLMModels are registered in the factory. Cannot create DummyLLM.")
|
|
69
|
+
|
|
70
|
+
super().__init__(model=dummy_model_instance, llm_config=LLMConfig())
|
|
71
|
+
|
|
72
|
+
def configure_system_prompt(self, system_prompt: str):
|
|
73
|
+
# This is on BaseLLM. My no-op implementation is fine.
|
|
74
|
+
super().configure_system_prompt(system_prompt)
|
|
75
|
+
|
|
76
|
+
async def _send_user_message_to_llm(self, user_message: str, image_urls: Optional[List[str]] = None, **kwargs) -> CompleteResponse:
|
|
77
|
+
"""Dummy implementation for sending a message."""
|
|
78
|
+
logger.debug("DummyLLM._send_user_message_to_llm called.")
|
|
79
|
+
return CompleteResponse(content="This is a dummy response from a dummy LLM.", usage=None)
|
|
80
|
+
|
|
81
|
+
async def _stream_user_message_to_llm(
|
|
82
|
+
self, user_message: str, image_urls: Optional[List[str]] = None, **kwargs
|
|
83
|
+
) -> AsyncIterator[ChunkResponse]:
|
|
84
|
+
"""Dummy implementation for streaming a message."""
|
|
85
|
+
logger.debug("DummyLLM._stream_user_message_to_llm called.")
|
|
86
|
+
yield ChunkResponse(content="This is a dummy response from a dummy LLM.", is_complete=True, usage=None)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def setup_logging(debug: bool = False):
|
|
90
|
+
"""Configures logging for the script."""
|
|
91
|
+
log_level = logging.DEBUG if debug else logging.INFO
|
|
92
|
+
root_logger = logging.getLogger()
|
|
93
|
+
if root_logger.hasHandlers():
|
|
94
|
+
for handler in root_logger.handlers[:]:
|
|
95
|
+
root_logger.removeHandler(handler)
|
|
96
|
+
logging.basicConfig(
|
|
97
|
+
level=log_level,
|
|
98
|
+
format='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
|
|
99
|
+
stream=sys.stdout,
|
|
100
|
+
)
|
|
101
|
+
if debug:
|
|
102
|
+
logging.getLogger("autobyteus").setLevel(logging.DEBUG)
|
|
103
|
+
logger.info("Debug logging enabled.")
|
|
104
|
+
else:
|
|
105
|
+
logging.getLogger("autobyteus").setLevel(logging.INFO)
|
|
106
|
+
|
|
107
|
+
# --- Environment Variable Checks ---
|
|
108
|
+
def check_required_env_vars():
|
|
109
|
+
"""Checks for environment variables required by this example and returns them."""
|
|
110
|
+
required_vars = {
|
|
111
|
+
"script_path": "TEST_GOOGLE_SLIDES_MCP_SCRIPT_PATH",
|
|
112
|
+
"google_client_id": "GOOGLE_CLIENT_ID",
|
|
113
|
+
"google_client_secret": "GOOGLE_CLIENT_SECRET",
|
|
114
|
+
"google_refresh_token": "GOOGLE_REFRESH_TOKEN",
|
|
115
|
+
}
|
|
116
|
+
env_values = {}
|
|
117
|
+
missing_vars = []
|
|
118
|
+
for key, var_name in required_vars.items():
|
|
119
|
+
value = os.environ.get(var_name)
|
|
120
|
+
if not value:
|
|
121
|
+
missing_vars.append(var_name)
|
|
122
|
+
else:
|
|
123
|
+
env_values[key] = value
|
|
124
|
+
if missing_vars:
|
|
125
|
+
logger.error("This example requires the following environment variables to be set: %s", missing_vars)
|
|
126
|
+
sys.exit(1)
|
|
127
|
+
if not Path(env_values["script_path"]).exists():
|
|
128
|
+
logger.error(f"The script path specified by TEST_GOOGLE_SLIDES_MCP_SCRIPT_PATH does not exist: {env_values['script_path']}")
|
|
129
|
+
sys.exit(1)
|
|
130
|
+
return env_values
|
|
131
|
+
|
|
132
|
+
def print_tool_definitions(tool_definitions: List[ToolDefinition]):
|
|
133
|
+
"""Iterates through a list of tool definitions and prints their JSON schema."""
|
|
134
|
+
print("\n--- Registered Tool Schemas (from ToolDefinition) ---")
|
|
135
|
+
for tool_definition in sorted(tool_definitions, key=lambda d: d.name):
|
|
136
|
+
try:
|
|
137
|
+
tool_json_schema = tool_definition.get_usage_json()
|
|
138
|
+
print(f"\n# Tool: {tool_definition.name}")
|
|
139
|
+
print(json.dumps(tool_json_schema, indent=2))
|
|
140
|
+
except Exception as e:
|
|
141
|
+
print(f"\n# Tool: {tool_definition.name}")
|
|
142
|
+
print(f" Error getting schema from definition: {e}")
|
|
143
|
+
print("\n--------------------------------------------------------\n")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
async def main():
|
|
147
|
+
"""
|
|
148
|
+
Main function demonstrating the full end-to-end MCP integration workflow.
|
|
149
|
+
"""
|
|
150
|
+
logger.info("--- Starting MCP Integration Workflow Example ---")
|
|
151
|
+
|
|
152
|
+
env_vars = check_required_env_vars()
|
|
153
|
+
|
|
154
|
+
# 1. Instantiate the core MCP and registry components.
|
|
155
|
+
tool_registry = default_tool_registry
|
|
156
|
+
registrar = McpToolRegistrar()
|
|
157
|
+
|
|
158
|
+
# 2. Define the configuration for the MCP server as a dictionary.
|
|
159
|
+
server_id = "google-slides-mcp"
|
|
160
|
+
google_slides_mcp_config_dict = {
|
|
161
|
+
server_id: {
|
|
162
|
+
"transport_type": "stdio",
|
|
163
|
+
"stdio_params": {
|
|
164
|
+
"command": "node",
|
|
165
|
+
"args": [env_vars["script_path"]],
|
|
166
|
+
"env": {
|
|
167
|
+
"GOOGLE_CLIENT_ID": env_vars["google_client_id"],
|
|
168
|
+
"GOOGLE_CLIENT_SECRET": env_vars["google_client_secret"],
|
|
169
|
+
"GOOGLE_REFRESH_TOKEN": env_vars["google_refresh_token"],
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
"enabled": True,
|
|
173
|
+
"tool_name_prefix": "gslides",
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
# 3. Discover and register tools by passing the config dictionary directly.
|
|
179
|
+
logger.info(f"Performing targeted discovery for remote tools from server '{server_id}'...")
|
|
180
|
+
await registrar.discover_and_register_tools(mcp_config=google_slides_mcp_config_dict)
|
|
181
|
+
# Use the ToolRegistry to get tools by their source server ID.
|
|
182
|
+
registered_tool_defs = tool_registry.get_tools_by_mcp_server(server_id)
|
|
183
|
+
logger.info(f"Tool registration complete. Discovered tools: {[t.name for t in registered_tool_defs]}")
|
|
184
|
+
|
|
185
|
+
# 4. Create an instance of a specific tool using the ToolRegistry.
|
|
186
|
+
create_tool_name = "gslides_create_presentation"
|
|
187
|
+
summarize_tool_name = "gslides_summarize_presentation"
|
|
188
|
+
|
|
189
|
+
if create_tool_name not in tool_registry.list_tool_names():
|
|
190
|
+
logger.error(f"Tool '{create_tool_name}' was not found in the registry. Aborting.")
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
logger.info(f"Creating an instance of the '{create_tool_name}' tool from the registry...")
|
|
194
|
+
create_presentation_tool = tool_registry.create_tool(create_tool_name)
|
|
195
|
+
|
|
196
|
+
logger.info(f"Creating an instance of the '{summarize_tool_name}' tool from the registry...")
|
|
197
|
+
summarize_presentation_tool = tool_registry.create_tool(summarize_tool_name)
|
|
198
|
+
|
|
199
|
+
# 5. Execute the tool using its standard .execute() method.
|
|
200
|
+
presentation_title = f"AutoByteUs E2E Demo - {datetime.now().isoformat()}"
|
|
201
|
+
logger.info(f"Executing '{create_tool_name}' with title: '{presentation_title}'")
|
|
202
|
+
|
|
203
|
+
dummy_llm = DummyLLM()
|
|
204
|
+
dummy_config = AgentConfig(
|
|
205
|
+
name="mcp_example_runner_agent",
|
|
206
|
+
role="tool_runner",
|
|
207
|
+
description="A dummy agent config for running tools outside of a full agent.",
|
|
208
|
+
llm_instance=dummy_llm,
|
|
209
|
+
system_prompt="N/A",
|
|
210
|
+
tools=[]
|
|
211
|
+
)
|
|
212
|
+
dummy_state = AgentRuntimeState(agent_id="mcp_example_runner")
|
|
213
|
+
dummy_context = AgentContext(agent_id="mcp_example_runner", config=dummy_config, state=dummy_state)
|
|
214
|
+
|
|
215
|
+
create_result = await create_presentation_tool.execute(
|
|
216
|
+
context=dummy_context,
|
|
217
|
+
title=presentation_title
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if not isinstance(create_result, str):
|
|
221
|
+
raise ValueError(f"Unexpected result type from tool '{create_tool_name}'. Expected a JSON string. Got: {type(create_result)}")
|
|
222
|
+
|
|
223
|
+
presentation_object = json.loads(create_result)
|
|
224
|
+
actual_presentation_id = presentation_object.get("presentationId")
|
|
225
|
+
|
|
226
|
+
if not actual_presentation_id:
|
|
227
|
+
raise ValueError(f"Could not find 'presentationId' in the response. Response: {create_result[:200]}...")
|
|
228
|
+
|
|
229
|
+
logger.info(f"Tool '{create_tool_name}' executed. Extracted Presentation ID: {actual_presentation_id}")
|
|
230
|
+
|
|
231
|
+
# 6. Execute the second tool.
|
|
232
|
+
logger.info(f"Executing '{summarize_tool_name}' for presentation ID: {actual_presentation_id}")
|
|
233
|
+
summary_result = await summarize_presentation_tool.execute(
|
|
234
|
+
context=dummy_context,
|
|
235
|
+
presentationId=actual_presentation_id
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if not isinstance(summary_result, str):
|
|
239
|
+
raise ValueError(f"Unexpected result type from tool '{summarize_tool_name}'. Got: {type(summary_result)}")
|
|
240
|
+
|
|
241
|
+
logger.info(f"Tool '{summarize_tool_name}' executed successfully.")
|
|
242
|
+
print("\n--- Presentation Summary ---")
|
|
243
|
+
print(summary_result)
|
|
244
|
+
print("--------------------------\n")
|
|
245
|
+
|
|
246
|
+
# 7. Print all tool schemas for verification
|
|
247
|
+
print_tool_definitions(registered_tool_defs)
|
|
248
|
+
|
|
249
|
+
except Exception as e:
|
|
250
|
+
logger.error(f"An error occurred during the workflow: {e}", exc_info=True)
|
|
251
|
+
|
|
252
|
+
logger.info("--- MCP Integration Workflow Example Finished ---")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
if __name__ == "__main__":
|
|
256
|
+
parser = argparse.ArgumentParser(description="Run the full MCP registration and execution workflow.")
|
|
257
|
+
parser.add_argument("--debug", action="store_true", help="Enable debug level logging on the console.")
|
|
258
|
+
args = parser.parse_args()
|
|
259
|
+
|
|
260
|
+
setup_logging(debug=args.debug)
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
asyncio.run(main())
|
|
264
|
+
except (KeyboardInterrupt, SystemExit) as e:
|
|
265
|
+
if isinstance(e, SystemExit) and e.code == 0:
|
|
266
|
+
logger.info("Script exited normally.")
|
|
267
|
+
else:
|
|
268
|
+
logger.info(f"Script interrupted ({type(e).__name__}). Exiting.")
|
|
269
|
+
except Exception as e:
|
|
270
|
+
logger.error(f"An unhandled error occurred at the top level: {e}", exc_info=True)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# file: autobyteus/examples/run_mcp_list_tools.py
|
|
2
|
+
"""
|
|
3
|
+
This example script demonstrates how to use the McpToolRegistrar to connect
|
|
4
|
+
to a remote MCP server and list the available tools without registering or
|
|
5
|
+
executing them.
|
|
6
|
+
|
|
7
|
+
This is a "dry-run" or "preview" operation, useful for inspecting the
|
|
8
|
+
capabilities of a remote MCP server.
|
|
9
|
+
"""
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
import sys
|
|
13
|
+
import os
|
|
14
|
+
import json
|
|
15
|
+
import argparse
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import List
|
|
18
|
+
|
|
19
|
+
# --- Boilerplate to make the script runnable from the project root ---
|
|
20
|
+
|
|
21
|
+
# Ensure the autobyteus package is discoverable
|
|
22
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
23
|
+
PACKAGE_ROOT = SCRIPT_DIR.parent
|
|
24
|
+
if str(PACKAGE_ROOT) not in sys.path:
|
|
25
|
+
sys.path.insert(0, str(PACKAGE_ROOT))
|
|
26
|
+
|
|
27
|
+
# Load environment variables from .env file in the project root
|
|
28
|
+
try:
|
|
29
|
+
from dotenv import load_dotenv
|
|
30
|
+
env_file_path = PACKAGE_ROOT / ".env"
|
|
31
|
+
if env_file_path.exists():
|
|
32
|
+
load_dotenv(env_file_path)
|
|
33
|
+
print(f"Loaded environment variables from: {env_file_path}")
|
|
34
|
+
else:
|
|
35
|
+
print(f"Info: No .env file found at: {env_file_path}. Relying on exported environment variables.")
|
|
36
|
+
except ImportError:
|
|
37
|
+
print("Warning: python-dotenv not installed. Cannot load .env file.")
|
|
38
|
+
|
|
39
|
+
# --- Imports for the MCP Client Example ---
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
from autobyteus.tools.mcp import McpToolRegistrar
|
|
43
|
+
from autobyteus.tools.registry import ToolDefinition
|
|
44
|
+
except ImportError as e:
|
|
45
|
+
print(f"Error importing autobyteus components: {e}", file=sys.stderr)
|
|
46
|
+
print("Please ensure that the autobyteus library is installed and accessible in your PYTHONPATH.", file=sys.stderr)
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
|
|
49
|
+
# --- Basic Logging Setup ---
|
|
50
|
+
logger = logging.getLogger("mcp_list_tools_example")
|
|
51
|
+
|
|
52
|
+
def setup_logging(debug: bool = False):
|
|
53
|
+
"""Configures logging for the script."""
|
|
54
|
+
log_level = logging.DEBUG if debug else logging.INFO
|
|
55
|
+
root_logger = logging.getLogger()
|
|
56
|
+
if root_logger.hasHandlers():
|
|
57
|
+
for handler in root_logger.handlers[:]:
|
|
58
|
+
root_logger.removeHandler(handler)
|
|
59
|
+
logging.basicConfig(
|
|
60
|
+
level=log_level,
|
|
61
|
+
format='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
|
|
62
|
+
stream=sys.stdout,
|
|
63
|
+
)
|
|
64
|
+
if debug:
|
|
65
|
+
logging.getLogger("autobyteus").setLevel(logging.DEBUG)
|
|
66
|
+
logger.info("Debug logging enabled.")
|
|
67
|
+
else:
|
|
68
|
+
logging.getLogger("autobyteus").setLevel(logging.INFO)
|
|
69
|
+
|
|
70
|
+
# --- Environment Variable Checks ---
|
|
71
|
+
def check_required_env_vars():
|
|
72
|
+
"""Checks for environment variables required by the SQLite MCP server."""
|
|
73
|
+
required_vars = {
|
|
74
|
+
"script_path": "TEST_SQLITE_MCP_SCRIPT_PATH",
|
|
75
|
+
"db_path": "TEST_SQLITE_DB_PATH",
|
|
76
|
+
}
|
|
77
|
+
env_values = {}
|
|
78
|
+
missing_vars = []
|
|
79
|
+
for key, var_name in required_vars.items():
|
|
80
|
+
value = os.environ.get(var_name)
|
|
81
|
+
if not value:
|
|
82
|
+
missing_vars.append(var_name)
|
|
83
|
+
else:
|
|
84
|
+
env_values[key] = value
|
|
85
|
+
if missing_vars:
|
|
86
|
+
logger.error("This example requires the following environment variables to be set: %s", missing_vars)
|
|
87
|
+
logger.error("Example usage in your .env file:")
|
|
88
|
+
logger.error('TEST_SQLITE_MCP_SCRIPT_PATH="/path/to/mcp-database-server/dist/src/index.js"')
|
|
89
|
+
logger.error('TEST_SQLITE_DB_PATH="/path/to/your/database.db"')
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
|
|
92
|
+
script_path_obj = Path(env_values["script_path"])
|
|
93
|
+
if not script_path_obj.exists():
|
|
94
|
+
logger.error(f"The script path specified by TEST_SQLITE_MCP_SCRIPT_PATH does not exist: {script_path_obj}")
|
|
95
|
+
sys.exit(1)
|
|
96
|
+
|
|
97
|
+
db_path_obj = Path(env_values["db_path"])
|
|
98
|
+
if not db_path_obj.exists():
|
|
99
|
+
logger.error(f"The database path specified by TEST_SQLITE_DB_PATH does not exist: {db_path_obj}")
|
|
100
|
+
logger.error("Please ensure the database file is created before running this script.")
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
return env_values
|
|
104
|
+
|
|
105
|
+
def print_tool_definitions(tool_definitions: List[ToolDefinition]):
|
|
106
|
+
"""Iterates through a list of tool definitions and prints their JSON schema."""
|
|
107
|
+
print("\n--- Discovered Remote Tool Schemas (from ToolDefinition) ---")
|
|
108
|
+
for tool_definition in sorted(tool_definitions, key=lambda d: d.name):
|
|
109
|
+
try:
|
|
110
|
+
# get_usage_json() provides a provider-agnostic JSON schema representation
|
|
111
|
+
tool_json_schema = tool_definition.get_usage_json()
|
|
112
|
+
print(f"\n# Tool: {tool_definition.name}")
|
|
113
|
+
print(f" Description: {tool_definition.description}")
|
|
114
|
+
print("# Schema (JSON):")
|
|
115
|
+
# Pretty-print the JSON schema
|
|
116
|
+
print(json.dumps(tool_json_schema, indent=2))
|
|
117
|
+
except Exception as e:
|
|
118
|
+
print(f"\n# Tool: {tool_definition.name}")
|
|
119
|
+
print(f" Error getting schema from definition: {e}")
|
|
120
|
+
print("\n--------------------------------------------------------\n")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async def main():
|
|
124
|
+
"""
|
|
125
|
+
Main function to connect to the SQLite MCP server and list its tools.
|
|
126
|
+
"""
|
|
127
|
+
logger.info("--- Starting MCP Remote Tool Listing Example ---")
|
|
128
|
+
|
|
129
|
+
env_vars = check_required_env_vars()
|
|
130
|
+
|
|
131
|
+
# 1. Instantiate the McpToolRegistrar.
|
|
132
|
+
registrar = McpToolRegistrar()
|
|
133
|
+
|
|
134
|
+
# 2. Define the configuration for the SQLite MCP server.
|
|
135
|
+
server_id = "sqlite-mcp"
|
|
136
|
+
sqlite_mcp_config_dict = {
|
|
137
|
+
server_id: {
|
|
138
|
+
"transport_type": "stdio",
|
|
139
|
+
"stdio_params": {
|
|
140
|
+
"command": "node",
|
|
141
|
+
"args": [
|
|
142
|
+
env_vars["script_path"],
|
|
143
|
+
env_vars["db_path"],
|
|
144
|
+
],
|
|
145
|
+
"env": {}, # No specific env vars needed for the SQLite server itself
|
|
146
|
+
},
|
|
147
|
+
"enabled": True,
|
|
148
|
+
"tool_name_prefix": "sqlite",
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
# 3. Use the registrar's `list_remote_tools` method for a preview.
|
|
154
|
+
# This connects to the server, lists tools, and disconnects without
|
|
155
|
+
# adding them to the main tool registry.
|
|
156
|
+
logger.info(f"Connecting to remote server '{server_id}' to preview available tools...")
|
|
157
|
+
|
|
158
|
+
tool_definitions = await registrar.list_remote_tools(mcp_config=sqlite_mcp_config_dict)
|
|
159
|
+
|
|
160
|
+
# 4. Print the results.
|
|
161
|
+
if tool_definitions:
|
|
162
|
+
print_tool_definitions(tool_definitions)
|
|
163
|
+
logger.info(f"Successfully listed {len(tool_definitions)} tools from the remote server.")
|
|
164
|
+
else:
|
|
165
|
+
logger.warning("No tools were found on the remote server.")
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error(f"An error occurred while trying to list remote tools: {e}", exc_info=True)
|
|
169
|
+
|
|
170
|
+
logger.info("--- MCP Remote Tool Listing Example Finished ---")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if __name__ == "__main__":
|
|
174
|
+
parser = argparse.ArgumentParser(description="List available tools from the remote SQLite MCP server.")
|
|
175
|
+
parser.add_argument("--debug", action="store_true", help="Enable debug level logging on the console.")
|
|
176
|
+
args = parser.parse_args()
|
|
177
|
+
|
|
178
|
+
setup_logging(debug=args.debug)
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
asyncio.run(main())
|
|
182
|
+
except (KeyboardInterrupt, SystemExit) as e:
|
|
183
|
+
# Gracefully handle user interruption or normal exit.
|
|
184
|
+
if isinstance(e, SystemExit) and e.code == 0:
|
|
185
|
+
logger.info("Script exited normally.")
|
|
186
|
+
else:
|
|
187
|
+
logger.warning(f"Script interrupted ({type(e).__name__}). Exiting.")
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.error(f"An unhandled error occurred at the top level: {e}", exc_info=True)
|