open-swarm 0.1.1744942884__py3-none-any.whl → 0.1.1744943085__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {open_swarm-0.1.1744942884.dist-info → open_swarm-0.1.1744943085.dist-info}/METADATA +1 -1
  2. {open_swarm-0.1.1744942884.dist-info → open_swarm-0.1.1744943085.dist-info}/RECORD +44 -24
  3. swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py +1 -1
  4. swarm/blueprints/divine_code/blueprint_divine_code.py +1 -1
  5. swarm/blueprints/django_chat/blueprint_django_chat.py +1 -1
  6. swarm/blueprints/echocraft/blueprint_echocraft.py +1 -1
  7. swarm/blueprints/family_ties/blueprint_family_ties.py +1 -1
  8. swarm/blueprints/mcp_demo/blueprint_mcp_demo.py +30 -1
  9. swarm/blueprints/mission_improbable/blueprint_mission_improbable.py +1 -1
  10. swarm/blueprints/monkai_magic/blueprint_monkai_magic.py +1 -1
  11. swarm/blueprints/nebula_shellz/blueprint_nebula_shellz.py +29 -1
  12. swarm/blueprints/omniplex/blueprint_omniplex.py +28 -18
  13. swarm/blueprints/rue_code/blueprint_rue_code.py +1 -1
  14. swarm/blueprints/suggestion/blueprint_suggestion.py +1 -1
  15. swarm/blueprints/unapologetic_press/blueprint_unapologetic_press.py +1 -1
  16. swarm/core/agent_utils.py +21 -0
  17. swarm/core/blueprint_base.py +395 -0
  18. swarm/core/blueprint_discovery.py +128 -0
  19. swarm/core/blueprint_runner.py +59 -0
  20. swarm/core/blueprint_utils.py +17 -0
  21. swarm/core/build_launchers.py +14 -0
  22. swarm/core/build_swarm_wrapper.py +12 -0
  23. swarm/core/common_utils.py +12 -0
  24. swarm/core/config_loader.py +122 -0
  25. swarm/core/config_manager.py +274 -0
  26. swarm/core/output_utils.py +173 -0
  27. swarm/core/server_config.py +81 -0
  28. swarm/core/setup_wizard.py +103 -0
  29. swarm/core/slash_commands.py +17 -0
  30. swarm/core/spinner.py +100 -0
  31. swarm/core/swarm_api.py +68 -0
  32. swarm/core/swarm_cli.py +216 -0
  33. swarm/core/swarm_wrapper.py +29 -0
  34. swarm/core/utils/__init__.py +0 -0
  35. swarm/core/utils/logger.py +36 -0
  36. swarm/extensions/cli/commands/blueprint_management.py +3 -3
  37. swarm/extensions/cli/commands/config_management.py +5 -6
  38. swarm/extensions/cli/commands/edit_config.py +8 -6
  39. swarm/extensions/cli/commands/list_blueprints.py +1 -1
  40. swarm/extensions/cli/commands/validate_env.py +3 -3
  41. swarm/extensions/cli/commands/validate_envvars.py +6 -6
  42. {open_swarm-0.1.1744942884.dist-info → open_swarm-0.1.1744943085.dist-info}/WHEEL +0 -0
  43. {open_swarm-0.1.1744942884.dist-info → open_swarm-0.1.1744943085.dist-info}/entry_points.txt +0 -0
  44. {open_swarm-0.1.1744942884.dist-info → open_swarm-0.1.1744943085.dist-info}/licenses/LICENSE +0 -0
@@ -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)
@@ -0,0 +1,81 @@
1
+ """
2
+ Module for managing server configuration files, including saving and validation.
3
+
4
+ Provides utilities to save configurations to disk and ensure integrity of data.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import logging
10
+ from swarm.utils.redact import redact_sensitive_data
11
+
12
+ # Initialize logger for this module
13
+ logger = logging.getLogger(__name__)
14
+ DEBUG = os.getenv("DEBUG", "False").lower() in ("true", "1", "t") # Define DEBUG locally
15
+ logger.setLevel(logging.DEBUG if DEBUG else logging.INFO)
16
+ stream_handler = logging.StreamHandler()
17
+ formatter = logging.Formatter("[%(levelname)s] %(asctime)s - %(name)s - %(message)s")
18
+ stream_handler.setFormatter(formatter)
19
+ if not logger.handlers:
20
+ logger.addHandler(stream_handler)
21
+
22
+ def save_server_config(config: dict, file_path: str = None) -> None:
23
+ """
24
+ Saves the server configuration to a JSON file.
25
+
26
+ Args:
27
+ config (dict): The configuration dictionary to save.
28
+ file_path (str): The path to save the configuration file. Defaults to 'swarm_settings.json' in the current directory.
29
+
30
+ Raises:
31
+ ValueError: If the configuration is not a valid dictionary.
32
+ OSError: If there are issues writing to the file.
33
+ """
34
+ if not isinstance(config, dict):
35
+ logger.error("Provided configuration is not a dictionary.")
36
+ raise ValueError("Configuration must be a dictionary.")
37
+
38
+ if file_path is None:
39
+ file_path = os.path.join(os.getcwd(), "swarm_settings.json")
40
+
41
+ logger.debug(f"Saving server configuration to {file_path}")
42
+ try:
43
+ with open(file_path, "w") as file:
44
+ json.dump(config, file, indent=4)
45
+ logger.info(f"Configuration successfully saved to {file_path}")
46
+ logger.debug(f"Saved configuration: {redact_sensitive_data(config)}")
47
+ except OSError as e:
48
+ logger.error(f"Error saving configuration to {file_path}: {e}")
49
+ raise
50
+
51
+ def load_server_config(file_path: str = None) -> dict:
52
+ """
53
+ Loads the server configuration from a JSON file.
54
+
55
+ Args:
56
+ file_path (str): The path to the configuration file. Defaults to 'swarm_settings.json' in the current directory.
57
+
58
+ Returns:
59
+ dict: The loaded configuration dictionary.
60
+
61
+ Raises:
62
+ FileNotFoundError: If the configuration file does not exist.
63
+ ValueError: If the configuration is not a valid dictionary or JSON is invalid.
64
+ """
65
+ if file_path is None:
66
+ file_path = os.path.join(os.getcwd(), "swarm_settings.json")
67
+ logger.debug(f"Loading server configuration from {file_path}")
68
+ try:
69
+ with open(file_path, "r") as file:
70
+ config = json.load(file)
71
+ if not isinstance(config, dict):
72
+ logger.error("Loaded configuration is not a dictionary.")
73
+ raise ValueError("Configuration must be a dictionary.")
74
+ logger.debug(f"Loaded configuration: {redact_sensitive_data(config)}")
75
+ return config
76
+ except FileNotFoundError as e:
77
+ logger.error(f"Configuration file not found: {file_path}")
78
+ raise
79
+ except json.JSONDecodeError as e:
80
+ logger.error(f"Invalid JSON in configuration file {file_path}: {e}")
81
+ raise ValueError(f"Invalid JSON in configuration file: {e}")