open-swarm 0.1.1745125888__py3-none-any.whl → 0.1.1745125904__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.1745125888.dist-info → open_swarm-0.1.1745125904.dist-info}/METADATA +1 -1
- {open_swarm-0.1.1745125888.dist-info → open_swarm-0.1.1745125904.dist-info}/RECORD +13 -10
- swarm/core/blueprint_base.py +352 -42
- swarm/core/blueprint_discovery.py +1 -4
- swarm/core/blueprint_ux.py +39 -3
- swarm/core/config_loader.py +3 -3
- swarm/core/output_utils.py +21 -1
- swarm/utils/ansi_box.py +34 -0
- swarm/ux/ansi_box.py +43 -0
- swarm/ux/spinner.py +53 -0
- {open_swarm-0.1.1745125888.dist-info → open_swarm-0.1.1745125904.dist-info}/WHEEL +0 -0
- {open_swarm-0.1.1745125888.dist-info → open_swarm-0.1.1745125904.dist-info}/entry_points.txt +0 -0
- {open_swarm-0.1.1745125888.dist-info → open_swarm-0.1.1745125904.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: open-swarm
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.1745125904
|
4
4
|
Summary: Open Swarm: Orchestrating AI Agent Swarms with Django
|
5
5
|
Project-URL: Homepage, https://github.com/yourusername/open-swarm
|
6
6
|
Project-URL: Documentation, https://github.com/yourusername/open-swarm/blob/main/README.md
|
@@ -56,17 +56,17 @@ swarm/blueprints/whiskeytango_foxtrot/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCe
|
|
56
56
|
swarm/blueprints/whiskeytango_foxtrot/apps.py,sha256=V1QKvyb2Vz-EtDNhhNe4tw2W9LYhNDuiaIq_fAU4ilw,334
|
57
57
|
swarm/blueprints/whiskeytango_foxtrot/blueprint_whiskeytango_foxtrot.py,sha256=y5OUPe5-W0Bhm3nO0hBH31ucXuote3PBVgb0WeOQodE,16567
|
58
58
|
swarm/core/agent_utils.py,sha256=exKnbJEm1VRL270x6XqQXHtJhqD8ogY3ZBIGZO_tYUE,552
|
59
|
-
swarm/core/blueprint_base.py,sha256=
|
60
|
-
swarm/core/blueprint_discovery.py,sha256=
|
59
|
+
swarm/core/blueprint_base.py,sha256=ZXfgCqaAVprML0KS3njHNxgldrwOCSGZrcI-fj7HyZs,40178
|
60
|
+
swarm/core/blueprint_discovery.py,sha256=UyJuBq_u7KLQC2_I9KGioX-Bcbj2-qjhKke4ensk_Xw,5622
|
61
61
|
swarm/core/blueprint_runner.py,sha256=TIfcIFfW86gCIeYs67ePmurKRPrcGgVYmVFGbpNuojQ,2576
|
62
62
|
swarm/core/blueprint_utils.py,sha256=Ef_pu-RYomqzFjMg6LOSPSdbYFCbYXjEoSvK1OT49Eo,702
|
63
|
-
swarm/core/blueprint_ux.py,sha256=
|
63
|
+
swarm/core/blueprint_ux.py,sha256=Lk4VFsIYyZSiDhwpLTYMaPAtDdLoVIDHaMWg9HCN7bQ,4567
|
64
64
|
swarm/core/build_launchers.py,sha256=2NZRvX0A3jFN1mYZI5vbXkPRDoXgdUBdunruhSUogko,563
|
65
65
|
swarm/core/build_swarm_wrapper.py,sha256=c_9oR3To4M2cZyc1uYSiysHLhUGX5FkCAQk9AG7Va2Q,231
|
66
66
|
swarm/core/common_utils.py,sha256=jeKcN3lMdrpOYWIpErH3L5am13jHjaImpVvk2b0mps4,462
|
67
|
-
swarm/core/config_loader.py,sha256=
|
67
|
+
swarm/core/config_loader.py,sha256=iSIKmB8Oc6MsI7luWZwFAIG_FNPILwnUZowKi6B1wyk,5614
|
68
68
|
swarm/core/config_manager.py,sha256=DdrFHpTnEtZOZZdBZbL4hE3AGdaZFJQ9duaAfi7GhJw,10015
|
69
|
-
swarm/core/output_utils.py,sha256=
|
69
|
+
swarm/core/output_utils.py,sha256=9rr1MNZ6Kucm78oAVAJaehlGbvgH2LY41VHzjxN1kZM,8345
|
70
70
|
swarm/core/server_config.py,sha256=v2t7q22qZwMAPdiUZreQaLAy1706k3VbR8Wk0NCQuCQ,3224
|
71
71
|
swarm/core/session_logger.py,sha256=92I0IGwUsRsYEISsO1HBeVWPnbBWBC4UuUzk2KstBuk,1859
|
72
72
|
swarm/core/setup_wizard.py,sha256=yAZ7MOgc8ZGti2kjZ72G6QLFBI0lbhXAa7Wi7SeXDYo,4567
|
@@ -257,6 +257,7 @@ swarm/templates/rest_mode/components/top_bar.html,sha256=I-J0Bmmcr0c6_KXEEmM6mwT
|
|
257
257
|
swarm/templates/websocket_partials/final_system_message.html,sha256=hZeOez70ZvYsDrmq965kFzV6wh5dBkKkP2FTiXS0FWo,128
|
258
258
|
swarm/templates/websocket_partials/system_message.html,sha256=0eBzz9dJBmnwDwnh-X_7wDefSatbS1LepaLhXxN-qI4,171
|
259
259
|
swarm/templates/websocket_partials/user_message.html,sha256=-TjdT4-FKFVXeYsPglG3VayDYg1A2beE5gV6AQWu-00,149
|
260
|
+
swarm/utils/ansi_box.py,sha256=VatZxyCR83Z-taNE361vMhrm8rD-8htic5gI96V2HPg,1172
|
260
261
|
swarm/utils/color_utils.py,sha256=utIfZ6ptGEdpHxIZiZ4gtfo5lLqZKQL5g0F8mEwMhTo,1184
|
261
262
|
swarm/utils/context_utils.py,sha256=CsoyFALq88pcMYU8Fw1joHrssnjDuisaroEkGiDwX_8,19551
|
262
263
|
swarm/utils/disable_tracing.py,sha256=IWe60GGs3l_pZA_OLV6qJDcAHOM2tQkMzg0Xj_x5hRE,1400
|
@@ -268,6 +269,8 @@ swarm/utils/message_sequence.py,sha256=7Xa7YwGPgojfkrGcl-SbeR_BWwzXGDYNqAxq8F6Xh
|
|
268
269
|
swarm/utils/message_utils.py,sha256=oNTD7pfmnnu_Br24pR2VEX9afIZwFLwg2HJBLXs1blY,4074
|
269
270
|
swarm/utils/openai_patch.py,sha256=Qogheuyh2_MTR6gO-uKgf3lu2jQc7cS72Vctc7vyXtI,1330
|
270
271
|
swarm/utils/redact.py,sha256=vtqTztsxyBg-2qHMgAPi1lI5mJgZEb8Fqi1KKFk3bZM,2042
|
272
|
+
swarm/ux/ansi_box.py,sha256=6mG8MpqrvDXGYU87eKqtky-ib83yt_qk2wiwf9DU5rs,1558
|
273
|
+
swarm/ux/spinner.py,sha256=UouAin377z8sKfvgrIF8klp_bKbwsSCxd2jgelndn5Y,1613
|
271
274
|
swarm/views/__init__.py,sha256=AcLk0R7Y69FhIVgJK2hZs8M_gCR-h_5iqUywz89yuHM,1223
|
272
275
|
swarm/views/api_views.py,sha256=BbDEgI6Ftg-c-mMkE9DvRGZHIZ-WAZSfwqAB7j98WxM,1937
|
273
276
|
swarm/views/chat_views.py,sha256=6UUtEJKrM2k_wi9A6AfhbbvMYunjzpY22M6hOIXASjA,15695
|
@@ -276,8 +279,8 @@ swarm/views/message_views.py,sha256=sDUnXyqKXC8WwIIMAlWf00s2_a2T9c75Na5FvYMJwBM,
|
|
276
279
|
swarm/views/model_views.py,sha256=aAbU4AZmrOTaPeKMWtoKK7FPYHdaN3Zbx55JfKzYTRY,2937
|
277
280
|
swarm/views/utils.py,sha256=8Usc0g0L0NPegNAyY20tJBNBy-JLwODf4VmxV0yUtpw,3627
|
278
281
|
swarm/views/web_views.py,sha256=T1CKe-Nyv1C8aDt6QFTGWo_dkH7ojWAvS_QW9mZnZp0,7371
|
279
|
-
open_swarm-0.1.
|
280
|
-
open_swarm-0.1.
|
281
|
-
open_swarm-0.1.
|
282
|
-
open_swarm-0.1.
|
283
|
-
open_swarm-0.1.
|
282
|
+
open_swarm-0.1.1745125904.dist-info/METADATA,sha256=q8jtZKzgtpfD5DLbBjdCbEdzi3jwsKfx3Nn-WwJfJV8,26977
|
283
|
+
open_swarm-0.1.1745125904.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
284
|
+
open_swarm-0.1.1745125904.dist-info/entry_points.txt,sha256=fo28d0_zJrytRsh8QqkdlWQT_9lyAwYUx1WuSTDI3HM,177
|
285
|
+
open_swarm-0.1.1745125904.dist-info/licenses/LICENSE,sha256=BU9bwRlnOt_JDIb6OT55Q4leLZx9RArDLTFnlDIrBEI,1062
|
286
|
+
open_swarm-0.1.1745125904.dist-info/RECORD,,
|
swarm/core/blueprint_base.py
CHANGED
@@ -1,8 +1,24 @@
|
|
1
1
|
# --- REMOVE noisy debug/framework prints unless SWARM_DEBUG=1 ---
|
2
2
|
import os
|
3
|
+
from swarm.utils.general_utils import is_debug_enabled
|
3
4
|
|
4
5
|
def _should_debug():
|
5
|
-
|
6
|
+
# Standardize debug detection: SWARM_DEBUG, SWARM_LOGLEVEL, LOGLEVEL, LOG_LEVEL, DEBUG
|
7
|
+
import os
|
8
|
+
# Highest precedence: explicit SWARM_DEBUG=1 or true
|
9
|
+
debug_env = os.environ.get("SWARM_DEBUG")
|
10
|
+
if debug_env is not None:
|
11
|
+
return debug_env.lower() in ("1", "true", "yes", "on")
|
12
|
+
# Next: SWARM_LOGLEVEL or LOGLEVEL or LOG_LEVEL
|
13
|
+
for var in ("SWARM_LOGLEVEL", "LOGLEVEL", "LOG_LEVEL"):
|
14
|
+
val = os.environ.get(var)
|
15
|
+
if val and val.upper() == "DEBUG":
|
16
|
+
return True
|
17
|
+
# Next: DEBUG=1 or true
|
18
|
+
debug_std = os.environ.get("DEBUG")
|
19
|
+
if debug_std is not None:
|
20
|
+
return debug_std.lower() in ("1", "true", "yes", "on")
|
21
|
+
return False
|
6
22
|
|
7
23
|
def _debug_print(*args, **kwargs):
|
8
24
|
if _should_debug():
|
@@ -123,6 +139,16 @@ class BlueprintBase(ABC):
|
|
123
139
|
console = Console()
|
124
140
|
session_logger: 'SessionLogger' = None
|
125
141
|
|
142
|
+
def __init__(self, blueprint_id: str, config=None, config_path=None, **kwargs):
|
143
|
+
self.blueprint_id = blueprint_id
|
144
|
+
self.config_path = config_path
|
145
|
+
self._config = config if config is not None else None
|
146
|
+
self._llm_profile_name = None
|
147
|
+
self._llm_profile_data = None
|
148
|
+
self._markdown_output = None
|
149
|
+
self._load_configuration() # Ensure config is loaded during init
|
150
|
+
# Add any additional initialization logic here
|
151
|
+
|
126
152
|
def display_splash_screen(self, animated: bool = False):
|
127
153
|
"""Default splash screen. Subclasses can override for custom CLI/API branding."""
|
128
154
|
console = Console()
|
@@ -133,37 +159,133 @@ class BlueprintBase(ABC):
|
|
133
159
|
Loads blueprint configuration. This method is a stub for compatibility with tests that patch it.
|
134
160
|
In production, configuration is loaded via _load_and_process_config.
|
135
161
|
"""
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
162
|
+
import os
|
163
|
+
import json
|
164
|
+
from pathlib import Path
|
165
|
+
def redact(val):
|
166
|
+
if not isinstance(val, str) or len(val) <= 4:
|
167
|
+
return "****"
|
168
|
+
return val[:2] + "*" * (len(val)-4) + val[-2:]
|
169
|
+
def redact_dict(d):
|
170
|
+
if isinstance(d, dict):
|
171
|
+
return {k: (redact_dict(v) if not (isinstance(v, str) and ("key" in k.lower() or "token" in k.lower() or "secret" in k.lower())) else redact(v)) for k, v in d.items()}
|
172
|
+
elif isinstance(d, list):
|
173
|
+
return [redact_dict(item) for item in d]
|
174
|
+
return d
|
140
175
|
try:
|
141
|
-
if
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
176
|
+
if self._config is None:
|
177
|
+
try:
|
178
|
+
# --- Get config from the AppConfig instance (Django) ---
|
179
|
+
app_config_instance = apps.get_app_config('swarm')
|
180
|
+
if not hasattr(app_config_instance, 'config') or not app_config_instance.config:
|
181
|
+
raise ValueError("AppConfig for 'swarm' does not have a valid 'config' attribute.")
|
182
|
+
self._config = app_config_instance.config
|
183
|
+
print("[SWARM_CONFIG_DEBUG] Loaded config from Django AppConfig.")
|
184
|
+
except Exception as e:
|
185
|
+
if _should_debug():
|
186
|
+
logger.warning(f"Falling back to CLI/home config due to error: {e}")
|
187
|
+
# 1. CLI argument (not handled here, handled in cli_handler)
|
188
|
+
# 2. Current working directory (guard against missing CWD)
|
189
|
+
try:
|
190
|
+
cwd_config = Path.cwd() / "swarm_config.json"
|
191
|
+
print(f"[SWARM_CONFIG_DEBUG] Trying: {cwd_config}")
|
192
|
+
except Exception as e:
|
193
|
+
cwd_config = None
|
194
|
+
if _should_debug():
|
195
|
+
logger.warning(f"Unable to determine CWD for config lookup: {e}")
|
196
|
+
if cwd_config and cwd_config.exists():
|
197
|
+
print(f"[SWARM_CONFIG_DEBUG] Loaded: {cwd_config}")
|
198
|
+
with open(cwd_config, 'r') as f:
|
199
|
+
self._config = json.load(f)
|
200
|
+
# 3. XDG_CONFIG_HOME or ~/.config/swarm/swarm_config.json
|
201
|
+
elif os.environ.get("XDG_CONFIG_HOME"):
|
202
|
+
xdg_config = Path(os.environ["XDG_CONFIG_HOME"]) / "swarm" / "swarm_config.json"
|
203
|
+
print(f"[SWARM_CONFIG_DEBUG] Trying: {xdg_config}")
|
204
|
+
if xdg_config.exists():
|
205
|
+
print(f"[SWARM_CONFIG_DEBUG] Loaded: {xdg_config}")
|
206
|
+
with open(xdg_config, 'r') as f:
|
207
|
+
self._config = json.load(f)
|
208
|
+
elif (Path.home() / ".config/swarm/swarm_config.json").exists():
|
209
|
+
home_config = Path.home() / ".config/swarm/swarm_config.json"
|
210
|
+
print(f"[SWARM_CONFIG_DEBUG] Loaded: {home_config}")
|
211
|
+
with open(home_config, 'r') as f:
|
212
|
+
self._config = json.load(f)
|
213
|
+
# 4. Legacy fallback: ~/.swarm/swarm_config.json
|
214
|
+
elif (Path.home() / ".swarm/swarm_config.json").exists():
|
215
|
+
legacy_config = Path.home() / ".swarm/swarm_config.json"
|
216
|
+
print(f"[SWARM_CONFIG_DEBUG] Loaded: {legacy_config}")
|
217
|
+
with open(legacy_config, 'r') as f:
|
218
|
+
self._config = json.load(f)
|
219
|
+
# 5. Fallback: OPENAI_API_KEY envvar
|
220
|
+
elif os.environ.get("OPENAI_API_KEY"):
|
221
|
+
print("[SWARM_CONFIG_DEBUG] No config file found, using OPENAI_API_KEY from env.")
|
222
|
+
self._config = {
|
223
|
+
"llm": {"default": {"provider": "openai", "model": "gpt-3.5-turbo", "api_key": os.environ["OPENAI_API_KEY"]}},
|
224
|
+
"settings": {"default_llm_profile": "default", "default_markdown_output": True},
|
225
|
+
"blueprints": {},
|
226
|
+
"llm_profile": "default",
|
227
|
+
"mcpServers": {}
|
228
|
+
}
|
229
|
+
logger.info("No config file found, using default config with OPENAI_API_KEY for CLI mode.")
|
230
|
+
else:
|
231
|
+
print("[SWARM_CONFIG_DEBUG] No config file found and OPENAI_API_KEY is not set. Using empty config.")
|
232
|
+
self._config = {}
|
233
|
+
logger.warning("No config file found and OPENAI_API_KEY is not set. Using empty config. CLI blueprints may fail if LLM config is required.")
|
234
|
+
if self._config is not None:
|
235
|
+
self._config = _substitute_env_vars(self._config)
|
236
|
+
# Ensure self._config is always a dict
|
237
|
+
if self._config is None:
|
238
|
+
self._config = {}
|
239
|
+
settings_section = self._config.get("settings", {})
|
240
|
+
llm_section = self._config.get("llm", {})
|
241
|
+
|
242
|
+
# --- After config is loaded, set OpenAI client from config if possible ---
|
243
|
+
try:
|
244
|
+
llm_profiles = self._config.get("llm", {})
|
245
|
+
default_profile = llm_profiles.get("default", {})
|
246
|
+
base_url = default_profile.get("base_url")
|
247
|
+
api_key = default_profile.get("api_key")
|
248
|
+
# Expand env vars if present
|
249
|
+
import os
|
250
|
+
if base_url and base_url.startswith("${"):
|
251
|
+
var = base_url[2:-1]
|
252
|
+
base_url = os.environ.get(var, base_url)
|
253
|
+
if api_key and api_key.startswith("${"):
|
254
|
+
var = api_key[2:-1]
|
255
|
+
api_key = os.environ.get(var, api_key)
|
256
|
+
if base_url and api_key:
|
257
|
+
from openai import AsyncOpenAI
|
258
|
+
from agents import set_default_openai_client
|
259
|
+
_debug_print(f"[DEBUG] (config) Setting OpenAI client: base_url={base_url}, api_key={'set' if api_key else 'NOT SET'}")
|
260
|
+
client = AsyncOpenAI(base_url=base_url, api_key=api_key)
|
261
|
+
set_default_openai_client(client)
|
262
|
+
except Exception as e:
|
263
|
+
_debug_print(f"[DEBUG] Failed to set OpenAI client from config: {e}")
|
264
|
+
|
265
|
+
# --- Debug: Print and log redacted config ---
|
266
|
+
redacted_config = redact_dict(self._config)
|
267
|
+
logger.debug(f"Loaded config (redacted): {json.dumps(redacted_config, indent=2)}")
|
158
268
|
|
159
|
-
#
|
160
|
-
|
161
|
-
#
|
269
|
+
# --- Process LLM profile name and data ---
|
270
|
+
default_profile = settings_section.get("default_llm_profile") or "default"
|
271
|
+
# Only set self._llm_profile_name if explicitly provided in config
|
272
|
+
if "llm_profile" in self._config:
|
273
|
+
self._llm_profile_name = self._config["llm_profile"]
|
274
|
+
# Do NOT set self._llm_profile_name to default_profile here; let resolution logic handle fallback
|
275
|
+
if "profiles" in llm_section:
|
276
|
+
self._llm_profile_data = llm_section["profiles"].get(self._llm_profile_name, {})
|
277
|
+
else:
|
278
|
+
self._llm_profile_data = llm_section.get(self._llm_profile_name, {})
|
279
|
+
blueprint_specific_settings = self._config.get("blueprints", {}).get(self.blueprint_id, {})
|
280
|
+
global_markdown_setting = settings_section.get("default_markdown_output", True)
|
281
|
+
self._markdown_output = blueprint_specific_settings.get("markdown_output", global_markdown_setting)
|
282
|
+
logger.debug(f"Markdown output for '{self.blueprint_id}': {self._markdown_output}")
|
162
283
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
284
|
+
except ValueError as e:
|
285
|
+
logger.error(f"Configuration error for blueprint '{self.blueprint_id}': {e}", exc_info=True)
|
286
|
+
raise
|
287
|
+
except Exception as e:
|
288
|
+
logger.error(f"Unexpected error loading config for blueprint '{self.blueprint_id}': {e}", exc_info=True)
|
167
289
|
raise
|
168
290
|
|
169
291
|
def _load_and_process_config(self):
|
@@ -189,7 +311,7 @@ class BlueprintBase(ABC):
|
|
189
311
|
if not hasattr(app_config_instance, 'config') or not app_config_instance.config:
|
190
312
|
raise ValueError("AppConfig for 'swarm' does not have a valid 'config' attribute.")
|
191
313
|
self._config = app_config_instance.config
|
192
|
-
|
314
|
+
print("[SWARM_CONFIG_DEBUG] Loaded config from Django AppConfig.")
|
193
315
|
except Exception as e:
|
194
316
|
if _should_debug():
|
195
317
|
logger.warning(f"Falling back to CLI/home config due to error: {e}")
|
@@ -197,28 +319,37 @@ class BlueprintBase(ABC):
|
|
197
319
|
# 2. Current working directory (guard against missing CWD)
|
198
320
|
try:
|
199
321
|
cwd_config = Path.cwd() / "swarm_config.json"
|
322
|
+
print(f"[SWARM_CONFIG_DEBUG] Trying: {cwd_config}")
|
200
323
|
except Exception as e:
|
201
324
|
cwd_config = None
|
202
325
|
if _should_debug():
|
203
326
|
logger.warning(f"Unable to determine CWD for config lookup: {e}")
|
204
327
|
if cwd_config and cwd_config.exists():
|
328
|
+
print(f"[SWARM_CONFIG_DEBUG] Loaded: {cwd_config}")
|
205
329
|
with open(cwd_config, 'r') as f:
|
206
330
|
self._config = json.load(f)
|
207
331
|
# 3. XDG_CONFIG_HOME or ~/.config/swarm/swarm_config.json
|
208
332
|
elif os.environ.get("XDG_CONFIG_HOME"):
|
209
333
|
xdg_config = Path(os.environ["XDG_CONFIG_HOME"]) / "swarm" / "swarm_config.json"
|
334
|
+
print(f"[SWARM_CONFIG_DEBUG] Trying: {xdg_config}")
|
210
335
|
if xdg_config.exists():
|
336
|
+
print(f"[SWARM_CONFIG_DEBUG] Loaded: {xdg_config}")
|
211
337
|
with open(xdg_config, 'r') as f:
|
212
338
|
self._config = json.load(f)
|
213
339
|
elif (Path.home() / ".config/swarm/swarm_config.json").exists():
|
214
|
-
|
340
|
+
home_config = Path.home() / ".config/swarm/swarm_config.json"
|
341
|
+
print(f"[SWARM_CONFIG_DEBUG] Loaded: {home_config}")
|
342
|
+
with open(home_config, 'r') as f:
|
215
343
|
self._config = json.load(f)
|
216
344
|
# 4. Legacy fallback: ~/.swarm/swarm_config.json
|
217
345
|
elif (Path.home() / ".swarm/swarm_config.json").exists():
|
218
|
-
|
346
|
+
legacy_config = Path.home() / ".swarm/swarm_config.json"
|
347
|
+
print(f"[SWARM_CONFIG_DEBUG] Loaded: {legacy_config}")
|
348
|
+
with open(legacy_config, 'r') as f:
|
219
349
|
self._config = json.load(f)
|
220
350
|
# 5. Fallback: OPENAI_API_KEY envvar
|
221
351
|
elif os.environ.get("OPENAI_API_KEY"):
|
352
|
+
print("[SWARM_CONFIG_DEBUG] No config file found, using OPENAI_API_KEY from env.")
|
222
353
|
self._config = {
|
223
354
|
"llm": {"default": {"provider": "openai", "model": "gpt-3.5-turbo", "api_key": os.environ["OPENAI_API_KEY"]}},
|
224
355
|
"settings": {"default_llm_profile": "default", "default_markdown_output": True},
|
@@ -228,10 +359,11 @@ class BlueprintBase(ABC):
|
|
228
359
|
}
|
229
360
|
logger.info("No config file found, using default config with OPENAI_API_KEY for CLI mode.")
|
230
361
|
else:
|
362
|
+
print("[SWARM_CONFIG_DEBUG] No config file found and OPENAI_API_KEY is not set. Using empty config.")
|
231
363
|
self._config = {}
|
232
364
|
logger.warning("No config file found and OPENAI_API_KEY is not set. Using empty config. CLI blueprints may fail if LLM config is required.")
|
233
|
-
|
234
|
-
|
365
|
+
if self._config is not None:
|
366
|
+
self._config = _substitute_env_vars(self._config)
|
235
367
|
# Ensure self._config is always a dict
|
236
368
|
if self._config is None:
|
237
369
|
self._config = {}
|
@@ -267,12 +399,14 @@ class BlueprintBase(ABC):
|
|
267
399
|
|
268
400
|
# --- Process LLM profile name and data ---
|
269
401
|
default_profile = settings_section.get("default_llm_profile") or "default"
|
270
|
-
self._llm_profile_name
|
402
|
+
# Only set self._llm_profile_name if explicitly provided in config
|
403
|
+
if "llm_profile" in self._config:
|
404
|
+
self._llm_profile_name = self._config["llm_profile"]
|
405
|
+
# Do NOT set self._llm_profile_name to default_profile here; let resolution logic handle fallback
|
271
406
|
if "profiles" in llm_section:
|
272
407
|
self._llm_profile_data = llm_section["profiles"].get(self._llm_profile_name, {})
|
273
408
|
else:
|
274
409
|
self._llm_profile_data = llm_section.get(self._llm_profile_name, {})
|
275
|
-
|
276
410
|
blueprint_specific_settings = self._config.get("blueprints", {}).get(self.blueprint_id, {})
|
277
411
|
global_markdown_setting = settings_section.get("default_markdown_output", True)
|
278
412
|
self._markdown_output = blueprint_specific_settings.get("markdown_output", global_markdown_setting)
|
@@ -285,6 +419,77 @@ class BlueprintBase(ABC):
|
|
285
419
|
logger.error(f"Unexpected error loading config for blueprint '{self.blueprint_id}': {e}", exc_info=True)
|
286
420
|
raise
|
287
421
|
|
422
|
+
def _resolve_llm_profile(self):
|
423
|
+
"""Resolve the LLM profile for this blueprint using the following order:
|
424
|
+
1. If self._llm_profile_name is set, use it.
|
425
|
+
2. If config has 'llm_profile', use it.
|
426
|
+
3. If config['blueprints'][blueprint_id or stripped]['llm_profile'] is set, use it.
|
427
|
+
4. If settings.default_llm in self._config, use it.
|
428
|
+
5. If global swarm_config has blueprints.<BlueprintName>.llm_profile, use it.
|
429
|
+
6. If settings.default_llm in global config, use it.
|
430
|
+
7. If env var DEFAULT_LLM is set, use it.
|
431
|
+
8. Otherwise, use 'default'.
|
432
|
+
"""
|
433
|
+
# Use cached value if already resolved
|
434
|
+
if getattr(self, '_resolved_llm_profile', None):
|
435
|
+
return self._resolved_llm_profile
|
436
|
+
name = getattr(self, 'blueprint_id', None) or getattr(self, '__class__', type(self)).__name__
|
437
|
+
profile = None
|
438
|
+
import logging
|
439
|
+
logger = logging.getLogger(__name__)
|
440
|
+
logger.debug(f"[DEBUG _resolve_llm_profile] blueprint_id/name: {name}")
|
441
|
+
logger.debug(f"[DEBUG _resolve_llm_profile] self._config: {self._config}")
|
442
|
+
# 1. Explicit override
|
443
|
+
if getattr(self, '_llm_profile_name', None):
|
444
|
+
logger.debug(f"[DEBUG _resolve_llm_profile] Using programmatic override: {self._llm_profile_name}")
|
445
|
+
profile = self._llm_profile_name
|
446
|
+
# 2. Blueprint config (top-level)
|
447
|
+
elif self._config and self._config.get('llm_profile'):
|
448
|
+
logger.debug(f"[DEBUG _resolve_llm_profile] Using top-level config llm_profile: {self._config['llm_profile']}")
|
449
|
+
profile = self._config['llm_profile']
|
450
|
+
# 3. Blueprint config (per-blueprint section)
|
451
|
+
elif self._config and self._config.get('blueprints'):
|
452
|
+
logger.debug(f"[DEBUG _resolve_llm_profile] Checking per-blueprint config for: {name}")
|
453
|
+
bp_cfg = self._config['blueprints'].get(name) or self._config['blueprints'].get(name.replace('Blueprint',''))
|
454
|
+
logger.debug(f"[DEBUG _resolve_llm_profile] bp_cfg: {bp_cfg}")
|
455
|
+
if isinstance(bp_cfg, dict) and 'llm_profile' in bp_cfg:
|
456
|
+
logger.debug(f"[DEBUG _resolve_llm_profile] Using per-blueprint llm_profile: {bp_cfg['llm_profile']}")
|
457
|
+
profile = bp_cfg['llm_profile']
|
458
|
+
# 4. settings.default_llm in self._config
|
459
|
+
elif self._config and self._config.get('settings') and self._config['settings'].get('default_llm'):
|
460
|
+
profile = self._config['settings']['default_llm']
|
461
|
+
# 5. Global config lookup (blueprints.<BlueprintName>.llm_profile)
|
462
|
+
else:
|
463
|
+
global_config = None
|
464
|
+
try:
|
465
|
+
import json, os
|
466
|
+
from pathlib import Path
|
467
|
+
config_paths = [Path.cwd() / 'swarm_config.json', Path.home() / '.config/swarm/swarm_config.json']
|
468
|
+
for path in config_paths:
|
469
|
+
if path.exists():
|
470
|
+
with open(path) as f:
|
471
|
+
global_config = json.load(f)
|
472
|
+
break
|
473
|
+
except Exception:
|
474
|
+
global_config = None
|
475
|
+
if global_config and 'blueprints' in global_config:
|
476
|
+
bp_cfg = global_config['blueprints'].get(name) or global_config['blueprints'].get(name.replace('Blueprint',''))
|
477
|
+
if bp_cfg and 'llm_profile' in bp_cfg:
|
478
|
+
profile = bp_cfg['llm_profile']
|
479
|
+
# 6. settings.default_llm in global config
|
480
|
+
if not profile and global_config and 'settings' in global_config and global_config['settings'].get('default_llm'):
|
481
|
+
profile = global_config['settings']['default_llm']
|
482
|
+
# 7. Env var DEFAULT_LLM
|
483
|
+
if not profile:
|
484
|
+
import os
|
485
|
+
profile = os.environ.get('DEFAULT_LLM')
|
486
|
+
# 8. Otherwise, use 'default'
|
487
|
+
if not profile:
|
488
|
+
profile = 'default'
|
489
|
+
logger.debug(f"[DEBUG _resolve_llm_profile] Final resolved profile: {profile}")
|
490
|
+
self._resolved_llm_profile = profile
|
491
|
+
return profile
|
492
|
+
|
288
493
|
@property
|
289
494
|
def config(self) -> Dict[str, Any]:
|
290
495
|
"""Returns the loaded and processed Swarm configuration."""
|
@@ -299,7 +504,7 @@ class BlueprintBase(ABC):
|
|
299
504
|
Raises a clear error if provider is missing.
|
300
505
|
"""
|
301
506
|
llm_section = self._config.get("llm", {}) if self._config else {}
|
302
|
-
profile_name = self.
|
507
|
+
profile_name = self._resolve_llm_profile()
|
303
508
|
profile = llm_section.get(profile_name)
|
304
509
|
if not profile:
|
305
510
|
raise ValueError(f"LLM profile '{profile_name}' not found in config: {llm_section}")
|
@@ -310,9 +515,13 @@ class BlueprintBase(ABC):
|
|
310
515
|
@property
|
311
516
|
def llm_profile_name(self) -> str:
|
312
517
|
"""Returns the name of the LLM profile being used."""
|
313
|
-
|
314
|
-
|
315
|
-
|
518
|
+
return self._resolve_llm_profile()
|
519
|
+
|
520
|
+
@llm_profile_name.setter
|
521
|
+
def llm_profile_name(self, value: str):
|
522
|
+
self._llm_profile_name = value
|
523
|
+
if hasattr(self, '_resolved_llm_profile'):
|
524
|
+
del self._resolved_llm_profile
|
316
525
|
|
317
526
|
@property
|
318
527
|
def slash_commands(self):
|
@@ -341,6 +550,22 @@ class BlueprintBase(ABC):
|
|
341
550
|
return bool(settings["default_markdown_output"])
|
342
551
|
return False
|
343
552
|
|
553
|
+
@property
|
554
|
+
def splash(self) -> str:
|
555
|
+
"""
|
556
|
+
Plain text splash/description for API, docs, etc.
|
557
|
+
"""
|
558
|
+
title = self.metadata.get('title', 'Blueprint')
|
559
|
+
desc = self.metadata.get('description', '')
|
560
|
+
return f"{title}: {desc}"
|
561
|
+
|
562
|
+
def get_cli_splash(self, color='cyan', emoji='🤖') -> str:
|
563
|
+
"""
|
564
|
+
CLI splash with ANSI/emoji, only for terminal output.
|
565
|
+
"""
|
566
|
+
from swarm.utils.ansi_box import ansi_box
|
567
|
+
return ansi_box(self.splash, color=color, emoji=emoji)
|
568
|
+
|
344
569
|
def _get_model_instance(self, profile_name: str):
|
345
570
|
"""Retrieves or creates an LLM Model instance, respecting LITELLM_MODEL/DEFAULT_LLM if set."""
|
346
571
|
if not hasattr(self, '_model_instance_cache'):
|
@@ -374,7 +599,7 @@ class BlueprintBase(ABC):
|
|
374
599
|
from agents.models.openai_responses import OpenAIResponsesModel
|
375
600
|
model_instance = OpenAIResponsesModel(model=model_name, openai_client=client)
|
376
601
|
else:
|
377
|
-
from agents.models.
|
602
|
+
from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel
|
378
603
|
model_instance = OpenAIChatCompletionsModel(model=model_name, openai_client=client)
|
379
604
|
self._model_instance_cache[profile_name] = model_instance
|
380
605
|
return model_instance
|
@@ -382,7 +607,7 @@ class BlueprintBase(ABC):
|
|
382
607
|
def make_agent(self, name, instructions, tools, mcp_servers=None, **kwargs):
|
383
608
|
"""Factory for creating an Agent with the correct model instance from framework config."""
|
384
609
|
from agents import Agent # Ensure Agent is always in scope
|
385
|
-
model_instance = self._get_model_instance(self.
|
610
|
+
model_instance = self._get_model_instance(self._resolve_llm_profile())
|
386
611
|
return Agent(
|
387
612
|
name=name,
|
388
613
|
model=model_instance,
|
@@ -446,6 +671,26 @@ class BlueprintBase(ABC):
|
|
446
671
|
self.session_logger.close()
|
447
672
|
self.session_logger = None
|
448
673
|
|
674
|
+
def print_help(self):
|
675
|
+
"""
|
676
|
+
Print CLI usage/help for this blueprint. Subclasses can override for custom help.
|
677
|
+
"""
|
678
|
+
blueprint_name = getattr(self, 'blueprint_id', self.__class__.__name__)
|
679
|
+
print(f"\nUsage: {blueprint_name} [options] <prompt>\n")
|
680
|
+
print("Options:")
|
681
|
+
print(" -m, --model <model> Model to use for completions")
|
682
|
+
print(" -q, --quiet Non-interactive mode (only prints final output)")
|
683
|
+
print(" -o, --output <file> Output file")
|
684
|
+
print(" --project-doc <file> Include a markdown file as context")
|
685
|
+
print(" --full-context Load all project files as context")
|
686
|
+
print(" --approval <policy> Set approval policy for agent actions (suggest, auto-edit, full-auto)")
|
687
|
+
print(" --version Show version and exit")
|
688
|
+
print(" -h, --help Show this help message and exit\n")
|
689
|
+
print("Examples:")
|
690
|
+
print(f" {blueprint_name} \"Refactor all utils into a single module.\"")
|
691
|
+
print(f" {blueprint_name} --full-context \"Summarize all TODOs in the project.\"")
|
692
|
+
print(f" {blueprint_name} --approval full-auto \"Upgrade all dependencies and update the changelog.\"")
|
693
|
+
|
449
694
|
@abstractmethod
|
450
695
|
async def run(self, messages: List[Dict[str, Any]], **kwargs: Any) -> AsyncGenerator[Dict[str, Any], None]:
|
451
696
|
"""
|
@@ -457,3 +702,68 @@ class BlueprintBase(ABC):
|
|
457
702
|
pprint.pprint(dict(os.environ))
|
458
703
|
raise NotImplementedError("Subclasses must implement the 'run' method.")
|
459
704
|
yield {}
|
705
|
+
|
706
|
+
def _load_configuration(self):
|
707
|
+
"""
|
708
|
+
Loads blueprint configuration. This method is a stub for compatibility with tests that patch it.
|
709
|
+
In production, configuration is loaded via _load_and_process_config.
|
710
|
+
"""
|
711
|
+
import os
|
712
|
+
import json
|
713
|
+
from pathlib import Path
|
714
|
+
import traceback
|
715
|
+
try:
|
716
|
+
if self._config is None:
|
717
|
+
try:
|
718
|
+
if self.config_path is not None:
|
719
|
+
self.config_path = Path(self.config_path)
|
720
|
+
if self.config_path.exists():
|
721
|
+
if is_debug_enabled():
|
722
|
+
print(f"[DEBUG LOADER] Reading config from {self.config_path}")
|
723
|
+
raw = self.config_path.read_text()
|
724
|
+
if is_debug_enabled():
|
725
|
+
print(f"[DEBUG LOADER] Raw config contents:\n{raw}")
|
726
|
+
self._config = json.loads(raw)
|
727
|
+
assert isinstance(self._config, dict), f"Config not a dict: {type(self._config)}"
|
728
|
+
assert self._config, "Config loaded but is empty!"
|
729
|
+
else:
|
730
|
+
logger.warning(f"Config path {self.config_path} does not exist. Using empty config.")
|
731
|
+
self._config = {}
|
732
|
+
else:
|
733
|
+
# Try cwd, then default, then /mnt/models/open-swarm-mcp/swarm_config.json
|
734
|
+
cwd_path = Path(os.getcwd()) / "swarm_config.json"
|
735
|
+
if cwd_path.exists():
|
736
|
+
if is_debug_enabled():
|
737
|
+
print(f"[DEBUG LOADER] Reading config from {cwd_path}")
|
738
|
+
raw = cwd_path.read_text()
|
739
|
+
if is_debug_enabled():
|
740
|
+
print(f"[DEBUG LOADER] Raw config contents:\n{raw}")
|
741
|
+
self._config = json.loads(raw)
|
742
|
+
assert isinstance(self._config, dict), f"Config not a dict: {type(self._config)}"
|
743
|
+
assert self._config, "Config loaded but is empty!"
|
744
|
+
else:
|
745
|
+
# Fallback to /mnt/models/open-swarm-mcp/swarm_config.json
|
746
|
+
mnt_path = Path("/mnt/models/open-swarm-mcp/swarm_config.json")
|
747
|
+
if mnt_path.exists():
|
748
|
+
if is_debug_enabled():
|
749
|
+
print(f"[DEBUG LOADER] Reading config from {mnt_path}")
|
750
|
+
raw = mnt_path.read_text()
|
751
|
+
if is_debug_enabled():
|
752
|
+
print(f"[DEBUG LOADER] Raw config contents:\n{raw}")
|
753
|
+
self._config = json.loads(raw)
|
754
|
+
assert isinstance(self._config, dict), f"Config not a dict: {type(self._config)}"
|
755
|
+
assert self._config, "Config loaded but is empty!"
|
756
|
+
else:
|
757
|
+
self._config = {}
|
758
|
+
except Exception as e:
|
759
|
+
print(f"[FATAL CONFIG LOAD ERROR] {e}")
|
760
|
+
traceback.print_exc()
|
761
|
+
self._config = {}
|
762
|
+
# Ensure self._config is always a dict
|
763
|
+
if self._config is None:
|
764
|
+
self._config = {}
|
765
|
+
return self._config
|
766
|
+
|
767
|
+
except Exception as e:
|
768
|
+
logger.error(f"Unexpected error loading config for blueprint '{self.blueprint_id}': {e}", exc_info=True)
|
769
|
+
raise
|
@@ -17,10 +17,7 @@ try:
|
|
17
17
|
except ImportError:
|
18
18
|
# This logger call is now safe
|
19
19
|
logger.error("Failed to import BlueprintBase from swarm.core.blueprint_base. Using placeholder.", exc_info=True)
|
20
|
-
|
21
|
-
metadata: Dict[str, Any] = {}
|
22
|
-
def __init__(self, *args, **kwargs): pass
|
23
|
-
async def run(self, *args, **kwargs): pass
|
20
|
+
# REMOVED DUMMY BlueprintBase to prevent MRO/import bugs. Always import BlueprintBase from swarm.core.blueprint_base
|
24
21
|
|
25
22
|
|
26
23
|
class BlueprintLoadError(Exception):
|
swarm/core/blueprint_ux.py
CHANGED
@@ -15,11 +15,11 @@ class BlueprintUX:
|
|
15
15
|
box += "┗"+"━"*20
|
16
16
|
return box
|
17
17
|
|
18
|
-
# Integrate unique improvements from the feature branch
|
19
18
|
import time
|
20
19
|
import itertools
|
21
20
|
|
22
21
|
# Style presets
|
22
|
+
|
23
23
|
def get_style(style):
|
24
24
|
if style == "serious":
|
25
25
|
return {
|
@@ -56,7 +56,9 @@ class BlueprintUXImproved:
|
|
56
56
|
return spinner_states[state_idx % len(spinner_states)]
|
57
57
|
|
58
58
|
def summary(self, op_type, result_count, params):
|
59
|
-
|
59
|
+
# Enhanced: pretty param formatting
|
60
|
+
param_str = ', '.join(f'{k}={v!r}' for k, v in (params or {}).items()) if params else 'None'
|
61
|
+
return f"{op_type} | Results: {result_count} | Params: {param_str}"
|
60
62
|
|
61
63
|
def progress(self, current, total=None):
|
62
64
|
if total:
|
@@ -70,4 +72,38 @@ class BlueprintUXImproved:
|
|
70
72
|
header = "[Semantic Search Results]"
|
71
73
|
else:
|
72
74
|
header = "[Results]"
|
73
|
-
|
75
|
+
# Enhanced: visually distinct divider
|
76
|
+
divider = "\n" + ("=" * 40) + "\n"
|
77
|
+
return f"{header}{divider}" + "\n".join(results)
|
78
|
+
|
79
|
+
def ansi_emoji_box(self, title, content, summary=None, params=None, result_count=None, op_type=None, style=None, color=None, status=None):
|
80
|
+
"""
|
81
|
+
Unified ANSI/emoji box for search/analysis/fileops results.
|
82
|
+
Summarizes: operation, result count, params, and allows for periodic updates.
|
83
|
+
Supports color and status for advanced UX.
|
84
|
+
"""
|
85
|
+
style_conf = get_style(style or self.style)
|
86
|
+
# Color/status logic
|
87
|
+
color_map = {
|
88
|
+
"success": "92", # green
|
89
|
+
"info": "94", # blue
|
90
|
+
"warning": "93", # yellow
|
91
|
+
"error": "91", # red
|
92
|
+
None: "94", # default blue
|
93
|
+
}
|
94
|
+
ansi_color = color_map.get(status, color_map[None])
|
95
|
+
box = f"\033[{ansi_color}m" + style_conf["border_top"] + "\033[0m\n"
|
96
|
+
box += f"\033[{ansi_color}m{style_conf['border_side']} {style_conf['emoji']} {title}"
|
97
|
+
if op_type:
|
98
|
+
box += f" | {op_type}"
|
99
|
+
if result_count is not None:
|
100
|
+
box += f" | Results: {result_count}"
|
101
|
+
box += "\033[0m\n"
|
102
|
+
if params:
|
103
|
+
box += f"\033[{ansi_color}m{style_conf['border_side']} Params: {params}\033[0m\n"
|
104
|
+
if summary:
|
105
|
+
box += f"\033[{ansi_color}m{style_conf['border_side']} {summary}\033[0m\n"
|
106
|
+
for line in content.split('\n'):
|
107
|
+
box += f"\033[{ansi_color}m{style_conf['border_side']} {line}\033[0m\n"
|
108
|
+
box += f"\033[{ansi_color}m" + style_conf["border_bottom"] + "\033[0m"
|
109
|
+
return box
|
swarm/core/config_loader.py
CHANGED
@@ -9,15 +9,15 @@ from dotenv import load_dotenv
|
|
9
9
|
logger = logging.getLogger("swarm.config")
|
10
10
|
|
11
11
|
def _substitute_env_vars(value: Any) -> Any:
|
12
|
-
|
12
|
+
import os
|
13
13
|
if isinstance(value, str):
|
14
|
+
# Always expand env vars in any string
|
14
15
|
return os.path.expandvars(value)
|
15
16
|
elif isinstance(value, list):
|
16
17
|
return [_substitute_env_vars(item) for item in value]
|
17
18
|
elif isinstance(value, dict):
|
18
19
|
return {k: _substitute_env_vars(v) for k, v in value.items()}
|
19
|
-
|
20
|
-
return value
|
20
|
+
return value
|
21
21
|
|
22
22
|
def load_environment(project_root: Path):
|
23
23
|
"""Loads environment variables from a `.env` file located at the project root."""
|
swarm/core/output_utils.py
CHANGED
@@ -7,6 +7,7 @@ import logging
|
|
7
7
|
import os
|
8
8
|
import sys
|
9
9
|
from typing import List, Dict, Any
|
10
|
+
import re
|
10
11
|
|
11
12
|
# Optional import for markdown rendering
|
12
13
|
try:
|
@@ -106,7 +107,26 @@ def pretty_print_response(messages: List[Dict[str, Any]], use_markdown: bool = F
|
|
106
107
|
if msg_content:
|
107
108
|
# --- DEBUG PRINT ---
|
108
109
|
print(f"\n[DEBUG Assistant content found, printing/rendering... Rich={RICH_AVAILABLE}, Markdown={use_markdown}]", flush=True)
|
109
|
-
|
110
|
+
# --- CODE FENCE HIGHLIGHTING ---
|
111
|
+
if RICH_AVAILABLE and '```' in msg_content:
|
112
|
+
import re
|
113
|
+
code_fence_pattern = r"```([\w\d]*)\n([\s\S]*?)```"
|
114
|
+
matches = re.findall(code_fence_pattern, msg_content)
|
115
|
+
if matches:
|
116
|
+
from rich.syntax import Syntax
|
117
|
+
from rich.console import Console
|
118
|
+
console = Console()
|
119
|
+
for lang, code in matches:
|
120
|
+
syntax = Syntax(code, lang or "python", theme="monokai", line_numbers=False)
|
121
|
+
console.print(syntax)
|
122
|
+
# Optionally print any non-code parts
|
123
|
+
non_code = re.split(code_fence_pattern, msg_content)
|
124
|
+
for i, part in enumerate(non_code):
|
125
|
+
if i % 3 == 0 and part.strip():
|
126
|
+
print(part.strip(), flush=True)
|
127
|
+
else:
|
128
|
+
print(msg_content, flush=True)
|
129
|
+
elif use_markdown and RICH_AVAILABLE:
|
110
130
|
render_markdown(msg_content)
|
111
131
|
else:
|
112
132
|
print(msg_content, flush=True)
|
swarm/utils/ansi_box.py
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
import shutil
|
2
|
+
|
3
|
+
ANSI_COLORS = {
|
4
|
+
'cyan': '\033[96m',
|
5
|
+
'green': '\033[92m',
|
6
|
+
'yellow': '\033[93m',
|
7
|
+
'magenta': '\033[95m',
|
8
|
+
'blue': '\033[94m',
|
9
|
+
'red': '\033[91m',
|
10
|
+
'white': '\033[97m',
|
11
|
+
'grey': '\033[90m',
|
12
|
+
'reset': '\033[0m',
|
13
|
+
}
|
14
|
+
|
15
|
+
|
16
|
+
def ansi_box(text, color='cyan', emoji='🤖', width=None):
|
17
|
+
"""
|
18
|
+
Draw a fancy ANSI box around the given text, with color and emoji.
|
19
|
+
"""
|
20
|
+
lines = [line.rstrip() for line in text.strip('\n').split('\n')]
|
21
|
+
max_len = max(len(line) for line in lines)
|
22
|
+
if width is None:
|
23
|
+
try:
|
24
|
+
width = min(shutil.get_terminal_size((80, 20)).columns, max_len + 6)
|
25
|
+
except Exception:
|
26
|
+
width = max_len + 6
|
27
|
+
box_width = max(width, max_len + 6)
|
28
|
+
color_code = ANSI_COLORS.get(color, ANSI_COLORS['cyan'])
|
29
|
+
reset = ANSI_COLORS['reset']
|
30
|
+
top = f"{color_code}╔{'═' * (box_width-2)}╗{reset}"
|
31
|
+
title = f"{color_code}║ {emoji} {' ' * (box_width-6)}║{reset}"
|
32
|
+
content = [f"{color_code}║ {line.ljust(box_width-4)} ║{reset}" for line in lines]
|
33
|
+
bottom = f"{color_code}╚{'═' * (box_width-2)}╝{reset}"
|
34
|
+
return '\n'.join([top, title] + content + [bottom])
|
swarm/ux/ansi_box.py
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
import sys
|
2
|
+
|
3
|
+
def ansi_box(title, content, count=None, params=None, style='default', emoji=None):
|
4
|
+
"""
|
5
|
+
Print a visually distinct ANSI box summarizing search/analysis results.
|
6
|
+
- title: e.g. 'Searched filesystem', 'Analyzed code'
|
7
|
+
- content: main result string or list of strings
|
8
|
+
- count: result count (optional)
|
9
|
+
- params: dict of search parameters (optional)
|
10
|
+
- style: 'default', 'success', 'warning', etc.
|
11
|
+
- emoji: optional emoji prefix
|
12
|
+
"""
|
13
|
+
border = {
|
14
|
+
'default': '━',
|
15
|
+
'success': '━',
|
16
|
+
'warning': '━',
|
17
|
+
}.get(style, '━')
|
18
|
+
color = {
|
19
|
+
'default': '\033[36m', # Cyan
|
20
|
+
'success': '\033[32m', # Green
|
21
|
+
'warning': '\033[33m', # Yellow
|
22
|
+
}.get(style, '\033[36m')
|
23
|
+
reset = '\033[0m'
|
24
|
+
box_width = 80
|
25
|
+
lines = []
|
26
|
+
head = f"{emoji + ' ' if emoji else ''}{title}"
|
27
|
+
if count is not None:
|
28
|
+
head += f" | Results: {count}"
|
29
|
+
if params:
|
30
|
+
head += f" | Params: {params}"
|
31
|
+
lines.append(color + border * box_width + reset)
|
32
|
+
lines.append(color + f"{head[:box_width]:^{box_width}}" + reset)
|
33
|
+
lines.append(color + border * box_width + reset)
|
34
|
+
if isinstance(content, str):
|
35
|
+
content = [content]
|
36
|
+
for line in content:
|
37
|
+
for l in str(line).split('\n'):
|
38
|
+
lines.append(f"{l[:box_width]:<{box_width}}")
|
39
|
+
lines.append(color + border * box_width + reset)
|
40
|
+
print("\n".join(lines))
|
41
|
+
|
42
|
+
# Example usage:
|
43
|
+
# ansi_box('Searched filesystem', 'Found 12 files', count=12, params={'pattern': '*.py'}, style='success', emoji='💾')
|
swarm/ux/spinner.py
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
import sys
|
2
|
+
import threading
|
3
|
+
import time
|
4
|
+
|
5
|
+
class Spinner:
|
6
|
+
"""
|
7
|
+
Displays spinner states: Generating., Generating.., Generating..., Running...,
|
8
|
+
and switches to 'Taking longer than expected' after a timeout.
|
9
|
+
"""
|
10
|
+
def __init__(self, base_message="Generating", long_wait_timeout=8):
|
11
|
+
self.base_message = base_message
|
12
|
+
self.states = [".", "..", "...", "..", "."]
|
13
|
+
self.running = False
|
14
|
+
self.thread = None
|
15
|
+
self.long_wait_timeout = long_wait_timeout
|
16
|
+
self._long_wait = False
|
17
|
+
|
18
|
+
def start(self):
|
19
|
+
self.running = True
|
20
|
+
self.thread = threading.Thread(target=self._spin, daemon=True)
|
21
|
+
self.thread.start()
|
22
|
+
|
23
|
+
def stop(self):
|
24
|
+
self.running = False
|
25
|
+
if self.thread:
|
26
|
+
self.thread.join()
|
27
|
+
sys.stdout.write("\r" + " " * 80 + "\r")
|
28
|
+
sys.stdout.flush()
|
29
|
+
|
30
|
+
def _spin(self):
|
31
|
+
idx = 0
|
32
|
+
start_time = time.time()
|
33
|
+
while self.running:
|
34
|
+
if not self._long_wait and (time.time() - start_time > self.long_wait_timeout):
|
35
|
+
self._long_wait = True
|
36
|
+
if self._long_wait:
|
37
|
+
msg = f"{self.base_message}... Taking longer than expected"
|
38
|
+
else:
|
39
|
+
msg = f"{self.base_message}{self.states[idx % len(self.states)]}"
|
40
|
+
sys.stdout.write(f"\r{msg}")
|
41
|
+
sys.stdout.flush()
|
42
|
+
time.sleep(0.4)
|
43
|
+
idx += 1
|
44
|
+
|
45
|
+
def set_message(self, message):
|
46
|
+
self.base_message = message
|
47
|
+
self._long_wait = False
|
48
|
+
|
49
|
+
# Example usage:
|
50
|
+
# spinner = Spinner()
|
51
|
+
# spinner.start()
|
52
|
+
# ... do work ...
|
53
|
+
# spinner.stop()
|
File without changes
|
{open_swarm-0.1.1745125888.dist-info → open_swarm-0.1.1745125904.dist-info}/entry_points.txt
RENAMED
File without changes
|
{open_swarm-0.1.1745125888.dist-info → open_swarm-0.1.1745125904.dist-info}/licenses/LICENSE
RENAMED
File without changes
|