open-swarm 0.1.1744943015__py3-none-any.whl → 0.1.1744947037__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.
- {open_swarm-0.1.1744943015.dist-info → open_swarm-0.1.1744947037.dist-info}/METADATA +1 -1
- {open_swarm-0.1.1744943015.dist-info → open_swarm-0.1.1744947037.dist-info}/RECORD +25 -5
- swarm/core/agent_utils.py +21 -0
- swarm/core/blueprint_base.py +395 -0
- swarm/core/blueprint_discovery.py +128 -0
- swarm/core/blueprint_runner.py +59 -0
- swarm/core/blueprint_utils.py +17 -0
- swarm/core/build_launchers.py +14 -0
- swarm/core/build_swarm_wrapper.py +12 -0
- swarm/core/common_utils.py +12 -0
- swarm/core/config_loader.py +122 -0
- swarm/core/config_manager.py +274 -0
- swarm/core/output_utils.py +173 -0
- swarm/core/server_config.py +81 -0
- swarm/core/setup_wizard.py +103 -0
- swarm/core/slash_commands.py +17 -0
- swarm/core/spinner.py +100 -0
- swarm/core/swarm_api.py +68 -0
- swarm/core/swarm_cli.py +216 -0
- swarm/core/swarm_wrapper.py +29 -0
- swarm/core/utils/__init__.py +0 -0
- swarm/core/utils/logger.py +36 -0
- {open_swarm-0.1.1744943015.dist-info → open_swarm-0.1.1744947037.dist-info}/WHEEL +0 -0
- {open_swarm-0.1.1744943015.dist-info → open_swarm-0.1.1744947037.dist-info}/entry_points.txt +0 -0
- {open_swarm-0.1.1744943015.dist-info → open_swarm-0.1.1744947037.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
import PyInstaller.__main__
|
3
|
+
|
4
|
+
def build_executable(script, output_name):
|
5
|
+
PyInstaller.__main__.run([
|
6
|
+
script,
|
7
|
+
"--onefile",
|
8
|
+
"--name", output_name,
|
9
|
+
"--add-data", "swarm_config.json:." # Adjust if additional data is needed
|
10
|
+
])
|
11
|
+
|
12
|
+
if __name__ == "__main__":
|
13
|
+
build_executable("launchers/swarm_cli.py", "swarm-cli")
|
14
|
+
build_executable("launchers/swarm_rest.py", "swarm-rest")
|
@@ -0,0 +1,12 @@
|
|
1
|
+
"""
|
2
|
+
Common utilities potentially shared across blueprint extensions.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Any # Removed Dict, List as they weren't used
|
6
|
+
|
7
|
+
def get_agent_name(agent: Any) -> str:
|
8
|
+
"""Return the name of an agent from its attributes ('name' or '__name__')."""
|
9
|
+
return getattr(agent, "name", getattr(agent, "__name__", "<unknown>"))
|
10
|
+
|
11
|
+
# get_token_count has been moved to swarm.utils.context_utils
|
12
|
+
# Ensure imports in other files point to the correct location.
|
@@ -0,0 +1,122 @@
|
|
1
|
+
import json
|
2
|
+
import logging
|
3
|
+
import os
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Any, Dict, Optional, Union
|
6
|
+
|
7
|
+
from dotenv import load_dotenv
|
8
|
+
|
9
|
+
logger = logging.getLogger("swarm.config")
|
10
|
+
|
11
|
+
def _substitute_env_vars(value: Any) -> Any:
|
12
|
+
"""Recursively substitute environment variables in strings, lists, and dicts."""
|
13
|
+
if isinstance(value, str):
|
14
|
+
return os.path.expandvars(value)
|
15
|
+
elif isinstance(value, list):
|
16
|
+
return [_substitute_env_vars(item) for item in value]
|
17
|
+
elif isinstance(value, dict):
|
18
|
+
return {k: _substitute_env_vars(v) for k, v in value.items()}
|
19
|
+
else:
|
20
|
+
return value
|
21
|
+
|
22
|
+
def load_environment(project_root: Path):
|
23
|
+
"""Loads environment variables from a `.env` file located at the project root."""
|
24
|
+
dotenv_path = project_root / ".env"
|
25
|
+
logger.debug(f"Checking for .env file at: {dotenv_path}")
|
26
|
+
try:
|
27
|
+
if dotenv_path.is_file():
|
28
|
+
loaded = load_dotenv(dotenv_path=dotenv_path, override=True)
|
29
|
+
if loaded:
|
30
|
+
logger.debug(f".env file Loaded/Overridden at: {dotenv_path}")
|
31
|
+
else:
|
32
|
+
logger.debug(f"No .env file found at {dotenv_path}.")
|
33
|
+
except Exception as e:
|
34
|
+
logger.error(f"Error loading .env file '{dotenv_path}': {e}", exc_info=logger.level <= logging.DEBUG)
|
35
|
+
|
36
|
+
def load_full_configuration(
|
37
|
+
blueprint_class_name: str,
|
38
|
+
default_config_path: Path,
|
39
|
+
config_path_override: Optional[Union[str, Path]] = None,
|
40
|
+
profile_override: Optional[str] = None,
|
41
|
+
cli_config_overrides: Optional[Dict[str, Any]] = None,
|
42
|
+
) -> Dict[str, Any]:
|
43
|
+
"""
|
44
|
+
Loads and merges configuration settings from base file, blueprint specifics, profiles, and CLI overrides.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
blueprint_class_name (str): The name of the blueprint class (e.g., "MyBlueprint").
|
48
|
+
default_config_path (Path): The default path to the swarm_config.json file.
|
49
|
+
config_path_override (Optional[Union[str, Path]]): Path specified via CLI argument.
|
50
|
+
profile_override (Optional[str]): Profile specified via CLI argument.
|
51
|
+
cli_config_overrides (Optional[Dict[str, Any]]): Overrides provided via CLI argument.
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
Dict[str, Any]: The final, merged configuration dictionary.
|
55
|
+
|
56
|
+
Raises:
|
57
|
+
ValueError: If the configuration file has JSON errors or cannot be read.
|
58
|
+
FileNotFoundError: If a specific config_path_override is given but the file doesn't exist.
|
59
|
+
"""
|
60
|
+
config_path = Path(config_path_override) if config_path_override else default_config_path
|
61
|
+
logger.debug(f"Attempting to load base configuration from: {config_path}")
|
62
|
+
base_config = {}
|
63
|
+
if config_path.is_file():
|
64
|
+
try:
|
65
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
66
|
+
base_config = json.load(f)
|
67
|
+
logger.debug(f"Successfully loaded base configuration from: {config_path}")
|
68
|
+
except json.JSONDecodeError as e:
|
69
|
+
raise ValueError(f"Config Error: Failed to parse JSON in {config_path}: {e}") from e
|
70
|
+
except Exception as e:
|
71
|
+
raise ValueError(f"Config Error: Failed to read {config_path}: {e}") from e
|
72
|
+
else:
|
73
|
+
if config_path_override:
|
74
|
+
raise FileNotFoundError(f"Configuration Error: Specified config file not found: {config_path}")
|
75
|
+
else:
|
76
|
+
logger.warning(f"Default configuration file not found at {config_path}. Proceeding without base configuration.")
|
77
|
+
|
78
|
+
# 1. Start with base defaults
|
79
|
+
final_config = base_config.get("defaults", {}).copy()
|
80
|
+
logger.debug(f"Applied base defaults. Keys: {list(final_config.keys())}")
|
81
|
+
|
82
|
+
# 2. Merge base llm and mcpServers sections
|
83
|
+
if "llm" in base_config:
|
84
|
+
final_config.setdefault("llm", {}).update(base_config["llm"])
|
85
|
+
logger.debug("Merged base 'llm'.")
|
86
|
+
if "mcpServers" in base_config:
|
87
|
+
final_config.setdefault("mcpServers", {}).update(base_config["mcpServers"])
|
88
|
+
logger.debug("Merged base 'mcpServers'.")
|
89
|
+
|
90
|
+
# 3. Merge blueprint-specific settings
|
91
|
+
blueprint_settings = base_config.get("blueprints", {}).get(blueprint_class_name, {})
|
92
|
+
if blueprint_settings:
|
93
|
+
final_config.update(blueprint_settings)
|
94
|
+
logger.debug(f"Merged BP '{blueprint_class_name}' settings. Keys: {list(blueprint_settings.keys())}")
|
95
|
+
|
96
|
+
# 4. Determine and merge profile settings
|
97
|
+
# Priority: CLI > Blueprint Specific > Base Defaults > "default"
|
98
|
+
profile_in_bp_settings = blueprint_settings.get("default_profile")
|
99
|
+
profile_in_base_defaults = base_config.get("defaults", {}).get("default_profile")
|
100
|
+
profile_to_use = profile_override or profile_in_bp_settings or profile_in_base_defaults or "default"
|
101
|
+
logger.debug(f"Using profile: '{profile_to_use}'")
|
102
|
+
profile_settings = base_config.get("profiles", {}).get(profile_to_use, {})
|
103
|
+
if profile_settings:
|
104
|
+
final_config.update(profile_settings)
|
105
|
+
logger.debug(f"Merged profile '{profile_to_use}'. Keys: {list(profile_settings.keys())}")
|
106
|
+
elif profile_to_use != "default" and (profile_override or profile_in_bp_settings or profile_in_base_defaults):
|
107
|
+
logger.warning(f"Profile '{profile_to_use}' requested but not found.")
|
108
|
+
|
109
|
+
# 5. Merge CLI overrides (highest priority)
|
110
|
+
if cli_config_overrides:
|
111
|
+
final_config.update(cli_config_overrides)
|
112
|
+
logger.debug(f"Merged CLI overrides. Keys: {list(cli_config_overrides.keys())}")
|
113
|
+
|
114
|
+
# Ensure top-level keys exist
|
115
|
+
final_config.setdefault("llm", {})
|
116
|
+
final_config.setdefault("mcpServers", {})
|
117
|
+
|
118
|
+
# 6. Substitute environment variables in the final config
|
119
|
+
final_config = _substitute_env_vars(final_config)
|
120
|
+
logger.debug("Applied final env var substitution.")
|
121
|
+
|
122
|
+
return final_config
|
@@ -0,0 +1,274 @@
|
|
1
|
+
# src/swarm/extensions/config/config_manager.py
|
2
|
+
|
3
|
+
import json
|
4
|
+
import shutil
|
5
|
+
import sys
|
6
|
+
import logging
|
7
|
+
from typing import Any, Dict
|
8
|
+
|
9
|
+
from swarm.core.server_config import load_server_config, save_server_config
|
10
|
+
from swarm.utils.color_utils import color_text
|
11
|
+
from swarm.settings import DEBUG
|
12
|
+
from swarm.core.utils.logger import *
|
13
|
+
from swarm.extensions.cli.utils import (
|
14
|
+
prompt_user,
|
15
|
+
log_and_exit,
|
16
|
+
display_message
|
17
|
+
)
|
18
|
+
|
19
|
+
# Initialize logger for this module
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
logger.setLevel(logging.DEBUG if DEBUG else logging.INFO)
|
22
|
+
if not logger.handlers:
|
23
|
+
stream_handler = logging.StreamHandler()
|
24
|
+
formatter = logging.Formatter("[%(levelname)s] %(asctime)s - %(name)s - %(message)s")
|
25
|
+
stream_handler.setFormatter(formatter)
|
26
|
+
logger.addHandler(stream_handler)
|
27
|
+
|
28
|
+
CONFIG_BACKUP_SUFFIX = ".backup"
|
29
|
+
|
30
|
+
def resolve_placeholders(config):
|
31
|
+
# Recursively resolve placeholders in the config dict (env vars, etc.)
|
32
|
+
import os
|
33
|
+
import re
|
34
|
+
pattern = re.compile(r'\$\{([^}]+)\}')
|
35
|
+
def _resolve(val):
|
36
|
+
if isinstance(val, str):
|
37
|
+
def replacer(match):
|
38
|
+
var = match.group(1)
|
39
|
+
return os.environ.get(var, match.group(0))
|
40
|
+
return pattern.sub(replacer, val)
|
41
|
+
elif isinstance(val, dict):
|
42
|
+
return {k: _resolve(v) for k, v in val.items()}
|
43
|
+
elif isinstance(val, list):
|
44
|
+
return [_resolve(v) for v in val]
|
45
|
+
return val
|
46
|
+
return _resolve(config)
|
47
|
+
|
48
|
+
def backup_configuration(config_path: str) -> None:
|
49
|
+
"""
|
50
|
+
Create a backup of the existing configuration file.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
config_path (str): Path to the configuration file.
|
54
|
+
"""
|
55
|
+
backup_path = config_path + CONFIG_BACKUP_SUFFIX
|
56
|
+
try:
|
57
|
+
shutil.copy(config_path, backup_path)
|
58
|
+
logger.info(f"Configuration backup created at '{backup_path}'")
|
59
|
+
display_message(f"Backup of configuration created at '{backup_path}'", "info")
|
60
|
+
except Exception as e:
|
61
|
+
logger.error(f"Failed to create configuration backup: {e}")
|
62
|
+
display_message(f"Failed to create backup: {e}", "error")
|
63
|
+
sys.exit(1)
|
64
|
+
|
65
|
+
def load_config(config_path: str) -> Dict[str, Any]:
|
66
|
+
"""
|
67
|
+
Load the server configuration from a JSON file and resolve placeholders.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
config_path (str): Path to the configuration file.
|
71
|
+
|
72
|
+
Returns:
|
73
|
+
Dict[str, Any]: The resolved configuration.
|
74
|
+
|
75
|
+
Raises:
|
76
|
+
FileNotFoundError: If the configuration file does not exist.
|
77
|
+
ValueError: If the file contains invalid JSON or unresolved placeholders.
|
78
|
+
"""
|
79
|
+
try:
|
80
|
+
with open(config_path, "r") as file:
|
81
|
+
config = json.load(file)
|
82
|
+
logger.debug(f"Raw configuration loaded: {config}")
|
83
|
+
except FileNotFoundError:
|
84
|
+
logger.error(f"Configuration file not found at {config_path}")
|
85
|
+
display_message(f"Configuration file not found at {config_path}", "error")
|
86
|
+
sys.exit(1)
|
87
|
+
except json.JSONDecodeError as e:
|
88
|
+
logger.error(f"Invalid JSON in configuration file {config_path}: {e}")
|
89
|
+
display_message(f"Invalid JSON in configuration file {config_path}: {e}", "error")
|
90
|
+
sys.exit(1)
|
91
|
+
|
92
|
+
# Resolve placeholders recursively
|
93
|
+
try:
|
94
|
+
resolved_config = resolve_placeholders(config)
|
95
|
+
logger.debug(f"Configuration after resolving placeholders: {resolved_config}")
|
96
|
+
except Exception as e:
|
97
|
+
logger.error(f"Failed to resolve placeholders in configuration: {e}")
|
98
|
+
display_message(f"Failed to resolve placeholders in configuration: {e}", "error")
|
99
|
+
sys.exit(1)
|
100
|
+
|
101
|
+
return resolved_config
|
102
|
+
|
103
|
+
def save_config(config_path: str, config: Dict[str, Any]) -> None:
|
104
|
+
"""
|
105
|
+
Save the updated configuration to the config file.
|
106
|
+
|
107
|
+
Args:
|
108
|
+
config_path (str): Path to the configuration file.
|
109
|
+
config (Dict[str, Any]): Configuration dictionary to save.
|
110
|
+
|
111
|
+
Raises:
|
112
|
+
SystemExit: If saving the configuration fails.
|
113
|
+
"""
|
114
|
+
try:
|
115
|
+
with open(config_path, "w") as f:
|
116
|
+
json.dump(config, f, indent=4)
|
117
|
+
logger.info(f"Configuration saved to '{config_path}'")
|
118
|
+
display_message(f"Configuration saved to '{config_path}'", "info")
|
119
|
+
except Exception as e:
|
120
|
+
logger.error(f"Failed to save configuration: {e}")
|
121
|
+
display_message(f"Failed to save configuration: {e}", "error")
|
122
|
+
sys.exit(1)
|
123
|
+
|
124
|
+
def add_llm(config_path: str) -> None:
|
125
|
+
"""
|
126
|
+
Add a new LLM to the configuration.
|
127
|
+
|
128
|
+
Args:
|
129
|
+
config_path (str): Path to the configuration file.
|
130
|
+
"""
|
131
|
+
config = load_config(config_path)
|
132
|
+
display_message("Starting the process to add a new LLM.", "info")
|
133
|
+
|
134
|
+
while True:
|
135
|
+
llm_name = prompt_user("Enter the name of the new LLM (or type 'done' to finish)").strip()
|
136
|
+
display_message(f"User entered LLM name: {llm_name}", "info")
|
137
|
+
if llm_name.lower() == 'done':
|
138
|
+
display_message("Finished adding LLMs.", "info")
|
139
|
+
break
|
140
|
+
if not llm_name:
|
141
|
+
display_message("LLM name cannot be empty.", "error")
|
142
|
+
continue
|
143
|
+
|
144
|
+
if llm_name in config.get("llm", {}):
|
145
|
+
display_message(f"LLM '{llm_name}' already exists.", "warning")
|
146
|
+
continue
|
147
|
+
|
148
|
+
llm = {}
|
149
|
+
llm["provider"] = prompt_user("Enter the provider type (e.g., 'openai', 'ollama')").strip()
|
150
|
+
llm["model"] = prompt_user("Enter the model name (e.g., 'gpt-4')").strip()
|
151
|
+
llm["base_url"] = prompt_user("Enter the base URL for the API").strip()
|
152
|
+
llm_api_key_input = prompt_user("Enter the environment variable for the API key (e.g., 'OPENAI_API_KEY') or leave empty if not required").strip()
|
153
|
+
if llm_api_key_input:
|
154
|
+
llm["api_key"] = f"${{{llm_api_key_input}}}"
|
155
|
+
else:
|
156
|
+
llm["api_key"] = ""
|
157
|
+
try:
|
158
|
+
temperature_input = prompt_user("Enter the temperature (e.g., 0.7)").strip()
|
159
|
+
llm["temperature"] = float(temperature_input)
|
160
|
+
except ValueError:
|
161
|
+
display_message("Invalid temperature value. Using default 0.7.", "warning")
|
162
|
+
llm["temperature"] = 0.7
|
163
|
+
|
164
|
+
config.setdefault("llm", {})[llm_name] = llm
|
165
|
+
logger.info(f"Added LLM '{llm_name}' to configuration.")
|
166
|
+
display_message(f"LLM '{llm_name}' added.", "info")
|
167
|
+
|
168
|
+
backup_configuration(config_path)
|
169
|
+
save_config(config_path, config)
|
170
|
+
display_message("LLM configuration process completed.", "info")
|
171
|
+
|
172
|
+
def remove_llm(config_path: str, llm_name: str) -> None:
|
173
|
+
"""
|
174
|
+
Remove an existing LLM from the configuration.
|
175
|
+
|
176
|
+
Args:
|
177
|
+
config_path (str): Path to the configuration file.
|
178
|
+
llm_name (str): Name of the LLM to remove.
|
179
|
+
"""
|
180
|
+
config = load_config(config_path)
|
181
|
+
|
182
|
+
if llm_name not in config.get("llm", {}):
|
183
|
+
display_message(f"LLM '{llm_name}' does not exist.", "error")
|
184
|
+
return
|
185
|
+
|
186
|
+
confirm = prompt_user(f"Are you sure you want to remove LLM '{llm_name}'? (yes/no)").strip().lower()
|
187
|
+
if confirm not in ['yes', 'y']:
|
188
|
+
display_message("Operation cancelled.", "warning")
|
189
|
+
return
|
190
|
+
|
191
|
+
del config["llm"][llm_name]
|
192
|
+
backup_configuration(config_path)
|
193
|
+
save_config(config_path, config)
|
194
|
+
display_message(f"LLM '{llm_name}' has been removed.", "info")
|
195
|
+
logger.info(f"Removed LLM '{llm_name}' from configuration.")
|
196
|
+
|
197
|
+
def add_mcp_server(config_path: str) -> None:
|
198
|
+
"""
|
199
|
+
Add a new MCP server to the configuration.
|
200
|
+
|
201
|
+
Args:
|
202
|
+
config_path (str): Path to the configuration file.
|
203
|
+
"""
|
204
|
+
config = load_config(config_path)
|
205
|
+
display_message("Starting the process to add a new MCP server.", "info")
|
206
|
+
|
207
|
+
while True:
|
208
|
+
server_name = prompt_user("Enter the name of the new MCP server (or type 'done' to finish)").strip()
|
209
|
+
display_message(f"User entered MCP server name: {server_name}", "info")
|
210
|
+
if server_name.lower() == 'done':
|
211
|
+
display_message("Finished adding MCP servers.", "info")
|
212
|
+
break
|
213
|
+
if not server_name:
|
214
|
+
display_message("Server name cannot be empty.", "error")
|
215
|
+
continue
|
216
|
+
|
217
|
+
if server_name in config.get("mcpServers", {}):
|
218
|
+
display_message(f"MCP server '{server_name}' already exists.", "warning")
|
219
|
+
continue
|
220
|
+
|
221
|
+
server = {}
|
222
|
+
server["command"] = prompt_user("Enter the command to run the MCP server (e.g., 'npx', 'uvx')").strip()
|
223
|
+
args_input = prompt_user("Enter the arguments as a JSON array (e.g., [\"-y\", \"server-name\"])").strip()
|
224
|
+
try:
|
225
|
+
server["args"] = json.loads(args_input)
|
226
|
+
if not isinstance(server["args"], list):
|
227
|
+
raise ValueError
|
228
|
+
except ValueError:
|
229
|
+
display_message("Invalid arguments format. Using an empty list.", "warning")
|
230
|
+
server["args"] = []
|
231
|
+
|
232
|
+
env_vars = {}
|
233
|
+
add_env = prompt_user("Do you want to add environment variables? (yes/no)").strip().lower()
|
234
|
+
while add_env in ['yes', 'y']:
|
235
|
+
env_var = prompt_user("Enter the environment variable name").strip()
|
236
|
+
env_value = prompt_user(f"Enter the value or placeholder for '{env_var}' (e.g., '${{{env_var}_KEY}}')").strip()
|
237
|
+
if env_var and env_value:
|
238
|
+
env_vars[env_var] = env_value
|
239
|
+
add_env = prompt_user("Add another environment variable? (yes/no)").strip().lower()
|
240
|
+
|
241
|
+
server["env"] = env_vars
|
242
|
+
|
243
|
+
config.setdefault("mcpServers", {})[server_name] = server
|
244
|
+
logger.info(f"Added MCP server '{server_name}' to configuration.")
|
245
|
+
display_message(f"MCP server '{server_name}' added.", "info")
|
246
|
+
|
247
|
+
backup_configuration(config_path)
|
248
|
+
save_config(config_path, config)
|
249
|
+
display_message("MCP server configuration process completed.", "info")
|
250
|
+
|
251
|
+
def remove_mcp_server(config_path: str, server_name: str) -> None:
|
252
|
+
"""
|
253
|
+
Remove an existing MCP server from the configuration.
|
254
|
+
|
255
|
+
Args:
|
256
|
+
config_path (str): Path to the configuration file.
|
257
|
+
server_name (str): Name of the MCP server to remove.
|
258
|
+
"""
|
259
|
+
config = load_config(config_path)
|
260
|
+
|
261
|
+
if server_name not in config.get("mcpServers", {}):
|
262
|
+
display_message(f"MCP server '{server_name}' does not exist.", "error")
|
263
|
+
return
|
264
|
+
|
265
|
+
confirm = prompt_user(f"Are you sure you want to remove MCP server '{server_name}'? (yes/no)").strip().lower()
|
266
|
+
if confirm not in ['yes', 'y']:
|
267
|
+
display_message("Operation cancelled.", "warning")
|
268
|
+
return
|
269
|
+
|
270
|
+
del config["mcpServers"][server_name]
|
271
|
+
backup_configuration(config_path)
|
272
|
+
save_config(config_path, config)
|
273
|
+
display_message(f"MCP server '{server_name}' has been removed.", "info")
|
274
|
+
logger.info(f"Removed MCP server '{server_name}' from configuration.")
|
@@ -0,0 +1,173 @@
|
|
1
|
+
"""
|
2
|
+
Output utilities for Swarm blueprints.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
import logging
|
7
|
+
import os
|
8
|
+
import sys
|
9
|
+
from typing import List, Dict, Any
|
10
|
+
|
11
|
+
# Optional import for markdown rendering
|
12
|
+
try:
|
13
|
+
from rich.markdown import Markdown
|
14
|
+
from rich.console import Console
|
15
|
+
from rich.panel import Panel
|
16
|
+
from rich.text import Text
|
17
|
+
from rich.rule import Rule
|
18
|
+
RICH_AVAILABLE = True
|
19
|
+
except ImportError:
|
20
|
+
RICH_AVAILABLE = False
|
21
|
+
|
22
|
+
logger = logging.getLogger(__name__)
|
23
|
+
|
24
|
+
def render_markdown(content: str) -> None:
|
25
|
+
"""Render markdown content using rich, if available."""
|
26
|
+
# --- DEBUG PRINT ---
|
27
|
+
print(f"\n[DEBUG render_markdown called with rich={RICH_AVAILABLE}]", flush=True)
|
28
|
+
if not RICH_AVAILABLE:
|
29
|
+
print(content, flush=True) # Fallback print with flush
|
30
|
+
return
|
31
|
+
console = Console()
|
32
|
+
md = Markdown(content)
|
33
|
+
console.print(md) # Rich handles flushing
|
34
|
+
|
35
|
+
def ansi_box(title: str, content: str, color: str = "94", emoji: str = "🔎", border: str = "─", width: int = 70) -> str:
|
36
|
+
"""Return a string or Panel with ANSI box formatting for search/analysis results using Rich if available."""
|
37
|
+
if RICH_AVAILABLE:
|
38
|
+
console = Console()
|
39
|
+
# Rich supports color names or hex, map color code to name
|
40
|
+
color_map = {
|
41
|
+
"94": "bright_blue",
|
42
|
+
"96": "bright_cyan",
|
43
|
+
"92": "bright_green",
|
44
|
+
"93": "bright_yellow",
|
45
|
+
"91": "bright_red",
|
46
|
+
"95": "bright_magenta",
|
47
|
+
"90": "grey82",
|
48
|
+
}
|
49
|
+
style = color_map.get(color, "bright_blue")
|
50
|
+
panel = Panel(
|
51
|
+
content,
|
52
|
+
title=f"{emoji} {title} {emoji}",
|
53
|
+
border_style=style,
|
54
|
+
width=width
|
55
|
+
)
|
56
|
+
# Return the rendered panel as a string for testability
|
57
|
+
with console.capture() as capture:
|
58
|
+
console.print(panel)
|
59
|
+
return capture.get()
|
60
|
+
# Fallback: legacy manual ANSI box
|
61
|
+
top = f"\033[{color}m{emoji} {border * (width - 4)} {emoji}\033[0m"
|
62
|
+
mid_title = f"\033[{color}m│ {title.center(width - 6)} │\033[0m"
|
63
|
+
lines = content.splitlines()
|
64
|
+
boxed = [top, mid_title, top]
|
65
|
+
for line in lines:
|
66
|
+
boxed.append(f"\033[{color}m│\033[0m {line.ljust(width - 6)} \033[{color}m│\033[0m")
|
67
|
+
boxed.append(top)
|
68
|
+
return "\n".join(boxed)
|
69
|
+
|
70
|
+
def print_search_box(title: str, content: str, color: str = "94", emoji: str = "🔎"):
|
71
|
+
print(ansi_box(title, content, color=color, emoji=emoji))
|
72
|
+
|
73
|
+
def pretty_print_response(messages: List[Dict[str, Any]], use_markdown: bool = False, spinner=None) -> None:
|
74
|
+
"""Format and print messages, optionally rendering assistant content as markdown."""
|
75
|
+
# --- DEBUG PRINT ---
|
76
|
+
print(f"\n[DEBUG pretty_print_response called with {len(messages)} messages, use_markdown={use_markdown}]", flush=True)
|
77
|
+
|
78
|
+
if spinner:
|
79
|
+
spinner.stop()
|
80
|
+
sys.stdout.write("\r\033[K") # Clear spinner line
|
81
|
+
sys.stdout.flush()
|
82
|
+
|
83
|
+
if not messages:
|
84
|
+
logger.debug("No messages to print in pretty_print_response.")
|
85
|
+
return
|
86
|
+
|
87
|
+
for i, msg in enumerate(messages):
|
88
|
+
# --- DEBUG PRINT ---
|
89
|
+
print(f"\n[DEBUG Processing message {i}: type={type(msg)}]", flush=True)
|
90
|
+
if not isinstance(msg, dict):
|
91
|
+
print(f"[DEBUG Skipping non-dict message {i}]", flush=True)
|
92
|
+
continue
|
93
|
+
|
94
|
+
role = msg.get("role")
|
95
|
+
sender = msg.get("sender", role if role else "Unknown")
|
96
|
+
msg_content = msg.get("content")
|
97
|
+
tool_calls = msg.get("tool_calls")
|
98
|
+
# --- DEBUG PRINT ---
|
99
|
+
print(f"[DEBUG Message {i}: role={role}, sender={sender}, has_content={bool(msg_content)}, has_tools={bool(tool_calls)}]", flush=True)
|
100
|
+
|
101
|
+
|
102
|
+
if role == "assistant":
|
103
|
+
print(f"\033[94m{sender}\033[0m: ", end="", flush=True)
|
104
|
+
if msg_content:
|
105
|
+
# --- DEBUG PRINT ---
|
106
|
+
print(f"\n[DEBUG Assistant content found, printing/rendering... Rich={RICH_AVAILABLE}, Markdown={use_markdown}]", flush=True)
|
107
|
+
if use_markdown and RICH_AVAILABLE:
|
108
|
+
render_markdown(msg_content)
|
109
|
+
else:
|
110
|
+
# --- DEBUG PRINT ---
|
111
|
+
print(f"\n[DEBUG Using standard print for content:]", flush=True)
|
112
|
+
print(msg_content, flush=True) # Added flush
|
113
|
+
elif not tool_calls:
|
114
|
+
print(flush=True) # Flush newline if no content/tools
|
115
|
+
|
116
|
+
if tool_calls and isinstance(tool_calls, list):
|
117
|
+
print(" \033[92mTool Calls:\033[0m", flush=True)
|
118
|
+
for tc in tool_calls:
|
119
|
+
if not isinstance(tc, dict): continue
|
120
|
+
func = tc.get("function", {})
|
121
|
+
tool_name = func.get("name", "Unnamed Tool")
|
122
|
+
args_str = func.get("arguments", "{}")
|
123
|
+
try: args_obj = json.loads(args_str); args_pretty = ", ".join(f"{k}={v!r}" for k, v in args_obj.items())
|
124
|
+
except json.JSONDecodeError: args_pretty = args_str
|
125
|
+
print(f" \033[95m{tool_name}\033[0m({args_pretty})", flush=True)
|
126
|
+
|
127
|
+
elif role == "tool":
|
128
|
+
tool_name = msg.get("tool_name", msg.get("name", "tool"))
|
129
|
+
tool_id = msg.get("tool_call_id", "N/A")
|
130
|
+
try: content_obj = json.loads(msg_content); pretty_content = json.dumps(content_obj, indent=2)
|
131
|
+
except (json.JSONDecodeError, TypeError): pretty_content = msg_content
|
132
|
+
print(f" \033[93m[{tool_name} Result ID: {tool_id}]\033[0m:\n {pretty_content.replace(chr(10), chr(10) + ' ')}", flush=True)
|
133
|
+
else:
|
134
|
+
# --- DEBUG PRINT ---
|
135
|
+
print(f"[DEBUG Skipping message {i} with role '{role}']", flush=True)
|
136
|
+
|
137
|
+
def print_terminal_command_result(cmd: str, result: dict, max_lines: int = 10):
|
138
|
+
"""
|
139
|
+
Render a terminal command result in the CLI with a shell prompt emoji, header, and Rich box.
|
140
|
+
- Header: 🐚 Ran terminal command
|
141
|
+
- Top line: colored, [basename(pwd)] > [cmd]
|
142
|
+
- Output: Rich Panel, max 10 lines, tailing if longer, show hint for toggle
|
143
|
+
"""
|
144
|
+
if not RICH_AVAILABLE:
|
145
|
+
# Fallback to simple print
|
146
|
+
print(f"🐚 Ran terminal command\n[{os.path.basename(result['cwd'])}] > {cmd}")
|
147
|
+
lines = result['output'].splitlines()
|
148
|
+
if len(lines) > max_lines:
|
149
|
+
lines = lines[-max_lines:]
|
150
|
+
print("[Output truncated. Showing last 10 lines.]")
|
151
|
+
print("\n".join(lines))
|
152
|
+
return
|
153
|
+
|
154
|
+
console = Console()
|
155
|
+
cwd_base = os.path.basename(result['cwd'])
|
156
|
+
header = Text(f"🐚 Ran terminal command", style="bold yellow")
|
157
|
+
subheader = Rule(f"[{cwd_base}] > {cmd}", style="bright_black")
|
158
|
+
lines = result['output'].splitlines()
|
159
|
+
truncated = False
|
160
|
+
if len(lines) > max_lines:
|
161
|
+
lines = lines[-max_lines:]
|
162
|
+
truncated = True
|
163
|
+
output_body = "\n".join(lines)
|
164
|
+
panel = Panel(
|
165
|
+
output_body,
|
166
|
+
title="Output",
|
167
|
+
border_style="cyan",
|
168
|
+
subtitle="[Output truncated. Showing last 10 lines. Press [t] to expand.]" if truncated else "",
|
169
|
+
width=80
|
170
|
+
)
|
171
|
+
console.print(header)
|
172
|
+
console.print(subheader)
|
173
|
+
console.print(panel)
|