open-swarm 0.1.1744936234__py3-none-any.whl → 0.1.1744936333__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: open-swarm
3
- Version: 0.1.1744936234
3
+ Version: 0.1.1744936333
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
@@ -1,13 +1,13 @@
1
1
  swarm/__init__.py,sha256=aT-yyX84tiZhihp0RAXYTADCo9dhOnmolQVfn4_NQa8,46
2
2
  swarm/apps.py,sha256=up4C3m2JeyXeUcH-wYeReCuiOBVJ6404w9OfaRChLwM,2568
3
3
  swarm/auth.py,sha256=8JIk1VbBvFFwOijEJAsrx6si802ZSMGnErXvmo0izUg,5935
4
- swarm/consumers.py,sha256=wESLamkhbi4SEZt9k3yx6eU9ufOIZMCAL-OAXjJBGXE,5056
4
+ swarm/consumers.py,sha256=rpPTLqNhxFuvtuPUwLSRpc4H-92yrx7ZtDr77ELR81Y,5996
5
5
  swarm/messages.py,sha256=CwADrjlj-uVmm-so1xIZvN1UkEWdzSn_hu7slfhuS8w,6549
6
6
  swarm/middleware.py,sha256=lPlHbFg9Rm9lUuvg026d4zTDjRMc8bQi0JegpGdqIZQ,3198
7
7
  swarm/models.py,sha256=Ix0WEYYqza2lbOEBNesikRCs3XGUPWmqQyMWzZYUaxM,1494
8
8
  swarm/permissions.py,sha256=iM86fSL1TtgqJzgDkS3Dl82X6Xk7VDHWwdBDfs5RKWc,1671
9
9
  swarm/serializers.py,sha256=4g3G2FdWpSIuLLC_SBKoNITw1b0G83Bxo7YHc-kjsro,4550
10
- swarm/settings.py,sha256=wrQoWfNylY_54z5c54x0TLe2Q9KEqvawNXjqCVhWuyI,6616
10
+ swarm/settings.py,sha256=yZzd_v1ux3POX6WPdgH8CQUF_0n5x720Kn8JQvCQgT4,6633
11
11
  swarm/tool_executor.py,sha256=KHM2mTGgbbTgWNN3fbV5c4MDY238OTLwaaqtkczFHFQ,12385
12
12
  swarm/urls.py,sha256=9eRQWsB-Vs3Nmes4mtlZtk_Rvuixf4Y9uwrX9dVQ9Is,3292
13
13
  swarm/util.py,sha256=G4x2hXopHhB7IdGCkUXGoykYWyiICnjxg7wcr-WqL8I,4644
@@ -50,22 +50,22 @@ swarm/blueprints/whiskeytango_foxtrot/apps.py,sha256=V1QKvyb2Vz-EtDNhhNe4tw2W9LY
50
50
  swarm/blueprints/whiskeytango_foxtrot/blueprint_whiskeytango_foxtrot.py,sha256=ithMeMjU6bMeX1AeaSBX22yRmu9yizfafGD4sjykoeo,14301
51
51
  swarm/extensions/__init__.py,sha256=SadbzfxckByaaqzuKPfXMvqmj45-dcMlavlfQYhGnzE,56
52
52
  swarm/extensions/blueprint/__init__.py,sha256=VHSlq8q3AeclMsp63f8RXc3vhcZyzHH0uEaYV6AW-ZI,1841
53
- swarm/extensions/blueprint/agent_utils.py,sha256=YWpANYTaFKAK4inWSirRlvzbHwQAH1wNkgW0esHKIeg,542
54
- swarm/extensions/blueprint/blueprint_base.py,sha256=xuwGq2tAGGevpXVkh_bfIuw1RYRJupeTHKPp8EfvEwI,5145
55
- swarm/extensions/blueprint/blueprint_discovery.py,sha256=oU7q1JeVMP5k52hfTGObDy1Jn2v5lJRufz9BNvP3W58,5531
53
+ swarm/extensions/blueprint/agent_utils.py,sha256=exKnbJEm1VRL270x6XqQXHtJhqD8ogY3ZBIGZO_tYUE,552
54
+ swarm/extensions/blueprint/blueprint_base.py,sha256=GI1vFcrU2oZpDpqnzWEZNe9O0jJpTfOXuAvmBfWYOcg,15435
55
+ swarm/extensions/blueprint/blueprint_discovery.py,sha256=v9lJeFDvPI919NzFjaCvmFBix5n0ceeL9y2JWGr_uLw,5720
56
56
  swarm/extensions/blueprint/blueprint_utils.py,sha256=Ef_pu-RYomqzFjMg6LOSPSdbYFCbYXjEoSvK1OT49Eo,702
57
- swarm/extensions/blueprint/cli_handler.py,sha256=ri812w0w2JzXtLSmwfSbpJCCveokgABnTfZEv4Iup2Y,9250
57
+ swarm/extensions/blueprint/cli_handler.py,sha256=kbF9G7sR5b5oD_t3rUijILZIog4hVMc-kR_ohWK0Mw0,8338
58
58
  swarm/extensions/blueprint/common_utils.py,sha256=jeKcN3lMdrpOYWIpErH3L5am13jHjaImpVvk2b0mps4,462
59
59
  swarm/extensions/blueprint/config_loader.py,sha256=ldQGtv4tXeDJzL2GCylDxykZxYBo4ALFY2kS0jZ79Eo,5652
60
60
  swarm/extensions/blueprint/django_utils.py,sha256=ObtkmF1JW4H2OEYa7vC6ussUsMBtDsZTTVeHGHI-GOQ,17457
61
61
  swarm/extensions/blueprint/interactive_mode.py,sha256=vGmMuAgC93TLjMi2RkXQ2FkWfIUblyOTFGHmVdGKLSQ,4572
62
- swarm/extensions/blueprint/output_utils.py,sha256=8OtVE3gEvPeeTu4Juo6Ad6omSlMqSuAtckXXx7P1CyQ,4022
62
+ swarm/extensions/blueprint/output_utils.py,sha256=HGpXIujoJNM5nCCzXH0Upog_ctw5BuftmMBiPujh-ZM,7139
63
63
  swarm/extensions/blueprint/runnable_blueprint.py,sha256=1MywZ54vUysLVtYmwCbcDYQmQnoZffCHgsArbe-VKe8,1813
64
- swarm/extensions/blueprint/spinner.py,sha256=3J0ZrNzoI5O5qR7hnCeRM3dZx2fLb_H3zkoj_AYt5LQ,3394
64
+ swarm/extensions/blueprint/spinner.py,sha256=9lyjzLnQBdEBy_dXr6N6I7nxx6KfrNp7wf44sQN06GU,3756
65
65
  swarm/extensions/blueprint/modes/rest_mode.py,sha256=KZuB_j2NfomER7CmlsLBqRipU3DymKY-9RpoGilMH0I,1357
66
66
  swarm/extensions/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
67
  swarm/extensions/cli/blueprint_runner.py,sha256=CG6XfOiDWuc84I_qefBpkwxEs7JcIbvNJqce9jBYUxo,9158
68
- swarm/extensions/cli/cli_args.py,sha256=Lws6Q3I-DNpzq9qSRN8ylUXg-lWly1SuqDSQF9n50bw,2980
68
+ swarm/extensions/cli/cli_args.py,sha256=z7jzrOFx8eBNW1N9ilKK07zN6VsTbjyLPp19tbmq9c8,3239
69
69
  swarm/extensions/cli/interactive_shell.py,sha256=ocHBP975uqJU5LQyM2IiMVW5lQ387lgYHe3sme9ucX4,1322
70
70
  swarm/extensions/cli/main.py,sha256=NnmovA7h1Rj1eDQVYe9AN6-eraIbIclHesNVmQPjdk4,1002
71
71
  swarm/extensions/cli/selection.py,sha256=etdG6hJFgnLuvD_sVJvXg8qFcgjzCjyL-vYyxWcU0TI,2002
@@ -80,7 +80,7 @@ swarm/extensions/cli/commands/validate_envvars.py,sha256=7-BDPYzV7wsga7kOggHNuCs
80
80
  swarm/extensions/cli/utils/discover_commands.py,sha256=aJdU3kSmLlpBxzGdfOA88AaCwpknHSD2cE0piCHZRUY,1053
81
81
  swarm/extensions/cli/utils/env_setup.py,sha256=k7QxRjzIGx5HC6RVZP9QSaaXEKMkcKCewD66u0e7qfE,496
82
82
  swarm/extensions/config/__init__.py,sha256=WjmGxMU5k3S40TNQxTfByYcT2YAchq_5gzXFWDLrLzU,141
83
- swarm/extensions/config/config_loader.py,sha256=CCT6U-iduNcB7hoeyPRonZR9veFzqoBGvEP2pweci2Y,4813
83
+ swarm/extensions/config/config_loader.py,sha256=q-zO8qnKudCfoVOKYak5RXbvozMYTygLtVgU4BGYPi4,4992
84
84
  swarm/extensions/config/config_manager.py,sha256=bBT-NGbRdsXoJ-lxZM5kjAf5FvVVGRVdbbCQJiCX1_0,9951
85
85
  swarm/extensions/config/server_config.py,sha256=iBlQOaFQmkEPNLmVpAQvXlSWhTsu-YMIjEf-H-7QfUU,1882
86
86
  swarm/extensions/config/setup_wizard.py,sha256=yAZ7MOgc8ZGti2kjZ72G6QLFBI0lbhXAa7Wi7SeXDYo,4567
@@ -92,7 +92,7 @@ swarm/extensions/launchers/build_swarm_wrapper.py,sha256=c_9oR3To4M2cZyc1uYSiysH
92
92
  swarm/extensions/launchers/swarm_api.py,sha256=f8olTI5JVdayp923etVQWsP8WRquPG5Mw3Q40ItN6kY,2877
93
93
  swarm/extensions/launchers/swarm_cli.py,sha256=dlvMq2HvUI2XlADuTzM8kpeedPkqzKB6k0oy7z2V_p0,9747
94
94
  swarm/extensions/launchers/swarm_wrapper.py,sha256=3K58yqPN4Ct0c7zfSDKRIGdL1Q7WOBXmAVHXT-aaPj4,1004
95
- swarm/llm/chat_completion.py,sha256=HnuuBtTooODJwU8egi3JSngmgo8076CBxF_0XqxDM2M,7634
95
+ swarm/llm/chat_completion.py,sha256=KR5ibJFZPBDKNuNIdlpushw6bcOVoY-jDeP-NJX4Qeg,9134
96
96
  swarm/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
97
97
  swarm/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
98
98
  swarm/management/commands/runserver.py,sha256=MQJxuV_4le4u_4RJu25OyiDgcEOyIB9StQ2U8xrkTOA,2621
@@ -252,8 +252,8 @@ swarm/views/message_views.py,sha256=sDUnXyqKXC8WwIIMAlWf00s2_a2T9c75Na5FvYMJwBM,
252
252
  swarm/views/model_views.py,sha256=aAbU4AZmrOTaPeKMWtoKK7FPYHdaN3Zbx55JfKzYTRY,2937
253
253
  swarm/views/utils.py,sha256=geX3Z5ZDKFYyXYBMilc-4qgOSjhujK3AfRtvbXgFpXk,3643
254
254
  swarm/views/web_views.py,sha256=ExQQeJpZ8CkLZQC_pXKOOmdnEy2qR3wEBP4LLp27DPU,7404
255
- open_swarm-0.1.1744936234.dist-info/METADATA,sha256=EVV17kQ_iNn0Xmnk7_VkMyLPBIWlbD08R6eDk3xQjko,13678
256
- open_swarm-0.1.1744936234.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
257
- open_swarm-0.1.1744936234.dist-info/entry_points.txt,sha256=z1UIVRRhri-V-hWxFkDEYu0SZPUIsVO4KpDaodgcFzU,125
258
- open_swarm-0.1.1744936234.dist-info/licenses/LICENSE,sha256=BU9bwRlnOt_JDIb6OT55Q4leLZx9RArDLTFnlDIrBEI,1062
259
- open_swarm-0.1.1744936234.dist-info/RECORD,,
255
+ open_swarm-0.1.1744936333.dist-info/METADATA,sha256=eXj_YZGiLtK_ixP5nKD6huvEqCqWuTvgB60MLdhfPXY,13678
256
+ open_swarm-0.1.1744936333.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
257
+ open_swarm-0.1.1744936333.dist-info/entry_points.txt,sha256=fo28d0_zJrytRsh8QqkdlWQT_9lyAwYUx1WuSTDI3HM,177
258
+ open_swarm-0.1.1744936333.dist-info/licenses/LICENSE,sha256=BU9bwRlnOt_JDIb6OT55Q4leLZx9RArDLTFnlDIrBEI,1062
259
+ open_swarm-0.1.1744936333.dist-info/RECORD,,
@@ -1,3 +1,4 @@
1
1
  [console_scripts]
2
+ codey = swarm.blueprints.codey.blueprint_codey:main
2
3
  swarm-api = swarm.extensions.launchers.swarm_api:main
3
4
  swarm-cli = swarm.extensions.launchers.swarm_cli:app
swarm/consumers.py CHANGED
@@ -62,6 +62,25 @@ class DjangoChatConsumer(AsyncWebsocketConsumer):
62
62
  await self.send(text_data=system_message_html)
63
63
 
64
64
  client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
65
+
66
+ # --- PATCH: Enforce LiteLLM-only endpoint and suppress OpenAI tracing/telemetry ---
67
+ import os, logging
68
+ if os.environ.get("LITELLM_BASE_URL") or os.environ.get("OPENAI_BASE_URL"):
69
+ logging.getLogger("openai.agents").setLevel(logging.CRITICAL)
70
+ try:
71
+ import openai.agents.tracing
72
+ openai.agents.tracing.TracingClient = lambda *a, **kw: None
73
+ except Exception:
74
+ pass
75
+ def _enforce_litellm_only(client):
76
+ base_url = getattr(client, 'base_url', None)
77
+ if base_url and 'openai.com' in base_url:
78
+ return
79
+ if base_url and 'openai.com' not in base_url:
80
+ import traceback
81
+ raise RuntimeError(f"Attempted fallback to OpenAI API when custom base_url is set! base_url={base_url}\n{traceback.format_stack()}")
82
+ _enforce_litellm_only(client)
83
+
65
84
  stream = await client.chat.completions.create(
66
85
  model=os.getenv("OPENAI_MODEL"),
67
86
  messages=self.messages,
@@ -6,7 +6,7 @@ This module has been updated to remove dependency on swarm.types;
6
6
  instead, it now imports Agent from the openai-agents SDK.
7
7
  """
8
8
 
9
- from agents.agent import Agent # Updated import
9
+ from blueprint_agents.agent import Agent # Updated import
10
10
 
11
11
  def get_agent_name(agent: Agent) -> str:
12
12
  """
@@ -1,3 +1,16 @@
1
+ # --- REMOVE noisy debug/framework prints unless SWARM_DEBUG=1 ---
2
+ import os
3
+
4
+ def _should_debug():
5
+ return os.environ.get("SWARM_DEBUG") == "1"
6
+
7
+ def _debug_print(*args, **kwargs):
8
+ if _should_debug():
9
+ print(*args, **kwargs)
10
+
11
+ def _framework_print(*args, **kwargs):
12
+ if _should_debug():
13
+ print(*args, **kwargs)
1
14
 
2
15
  # --- Content for src/swarm/extensions/blueprint/blueprint_base.py ---
3
16
  import logging
@@ -8,9 +21,98 @@ from pathlib import Path
8
21
  from django.apps import apps # Import Django apps registry
9
22
 
10
23
  # Keep the function import
11
- from swarm.extensions.config.config_loader import get_profile_from_config
24
+ from swarm.extensions.config.config_loader import get_profile_from_config, _substitute_env_vars
25
+
26
+ from openai import AsyncOpenAI
27
+ from agents import set_default_openai_client
28
+ from .slash_commands import slash_registry, SlashCommandRegistry
29
+ from blueprint_agents import * # Import all from blueprint_agents
12
30
 
13
31
  logger = logging.getLogger(__name__)
32
+ from rich.console import Console
33
+ import traceback
34
+
35
+ # --- PATCH: Suppress OpenAI tracing/telemetry errors if using LiteLLM/custom endpoint ---
36
+ import logging
37
+ import os
38
+ if os.environ.get("LITELLM_BASE_URL") or os.environ.get("OPENAI_BASE_URL"):
39
+ # Silence openai.agents tracing/telemetry errors
40
+ logging.getLogger("openai.agents").setLevel(logging.CRITICAL)
41
+ try:
42
+ import openai.agents.tracing
43
+ openai.agents.tracing.TracingClient = lambda *a, **kw: None
44
+ except Exception:
45
+ pass
46
+
47
+ # --- Spinner/Status Message Enhancements ---
48
+ # To be used by all blueprints for consistent UX
49
+ import itertools
50
+ import sys
51
+ import threading
52
+ import time
53
+
54
+ class Spinner:
55
+ def __init__(self, message_sequence=None, interval=0.3, slow_threshold=10):
56
+ self.message_sequence = message_sequence or ['Generating.', 'Generating..', 'Generating...', 'Running...']
57
+ self.interval = interval
58
+ self.slow_threshold = slow_threshold # seconds before 'Taking longer than expected'
59
+ self._stop_event = threading.Event()
60
+ self._thread = None
61
+ self._start_time = None
62
+
63
+ def start(self):
64
+ self._stop_event.clear()
65
+ self._start_time = time.time()
66
+ self._thread = threading.Thread(target=self._spin)
67
+ self._thread.start()
68
+
69
+ def _spin(self):
70
+ for msg in itertools.cycle(self.message_sequence):
71
+ if self._stop_event.is_set():
72
+ break
73
+ elapsed = time.time() - self._start_time
74
+ if elapsed > self.slow_threshold:
75
+ sys.stdout.write('\rGenerating... Taking longer than expected ')
76
+ else:
77
+ sys.stdout.write(f'\r{msg} ')
78
+ sys.stdout.flush()
79
+ time.sleep(self.interval)
80
+ sys.stdout.write('\r')
81
+ sys.stdout.flush()
82
+
83
+ def stop(self, final_message=''):
84
+ self._stop_event.set()
85
+ if self._thread:
86
+ self._thread.join()
87
+ if final_message:
88
+ sys.stdout.write(f'\r{final_message}\n')
89
+ sys.stdout.flush()
90
+
91
+ # Usage Example (to be called in blueprints):
92
+ # spinner = Spinner()
93
+ # spinner.start()
94
+ # ... do work ...
95
+ # spinner.stop('Done!')
96
+
97
+ def configure_openai_client_from_env():
98
+ """
99
+ Framework-level function: Always instantiate and set the default OpenAI client.
100
+ Prints out the config being used for debug.
101
+ """
102
+ import os
103
+ from agents import set_default_openai_client
104
+ from openai import AsyncOpenAI
105
+ base_url = os.environ.get("LITELLM_BASE_URL") or os.environ.get("OPENAI_BASE_URL")
106
+ api_key = os.environ.get("LITELLM_API_KEY") or os.environ.get("OPENAI_API_KEY")
107
+ _debug_print(f"[DEBUG] Using OpenAI client config: base_url={base_url}, api_key={'set' if api_key else 'NOT SET'}")
108
+ if base_url and api_key:
109
+ client = AsyncOpenAI(base_url=base_url, api_key=api_key)
110
+ set_default_openai_client(client)
111
+ _framework_print(f"[FRAMEWORK] Set default OpenAI client: base_url={base_url}, api_key={'set' if api_key else 'NOT SET'}")
112
+ else:
113
+ _framework_print("[FRAMEWORK] WARNING: base_url or api_key missing, OpenAI client not set!")
114
+
115
+ configure_openai_client_from_env()
14
116
 
15
117
  class BlueprintBase(ABC):
16
118
  """
@@ -18,53 +120,157 @@ class BlueprintBase(ABC):
18
120
 
19
121
  Defines the core interface for blueprint initialization and execution.
20
122
  """
21
- def __init__(self, blueprint_id: str, config_path: Optional[Path] = None):
22
- """
23
- Initializes the blueprint.
123
+ enable_terminal_commands: bool = False # By default, terminal command execution is disabled
24
124
 
25
- Args:
26
- blueprint_id: A unique identifier for this blueprint instance.
27
- config_path: Optional path to a specific swarm_config.json file.
28
- If None, the standard search logic will be used.
125
+ @classmethod
126
+ def main(cls):
29
127
  """
30
- if not blueprint_id:
31
- raise ValueError("blueprint_id cannot be empty or None") # Add validation
32
- self.blueprint_id = blueprint_id
33
- self.config_path = config_path # Note: config_path is currently unused if we rely on AppConfig
34
- self._config: Optional[Dict[str, Any]] = None
35
- self._llm_profile_name: Optional[str] = None
36
- self._llm_profile_data: Optional[Dict[str, Any]] = None
37
- self._markdown_output: bool = True # Default
38
-
39
- logger.info(f"Initializing blueprint '{self.blueprint_id}' (Type: {self.__class__.__name__})")
40
- self._load_and_process_config()
128
+ Standard CLI entry point for all blueprints.
129
+ Subclasses can override metadata/config_path if needed.
130
+ """
131
+ from swarm.extensions.blueprint.cli_handler import run_blueprint_cli
132
+ from pathlib import Path
133
+ swarm_version = getattr(cls, "SWARM_VERSION", "1.0.0")
134
+ config_path = getattr(cls, "DEFAULT_CONFIG_PATH", Path(__file__).parent / "swarm_config.json")
135
+ run_blueprint_cli(cls, swarm_version=swarm_version, default_config_path=config_path)
136
+
137
+ def display_splash_screen(self, animated: bool = False):
138
+ """Default splash screen. Subclasses can override for custom CLI/API branding."""
139
+ console = Console()
140
+ console.print(f"[bold cyan]Welcome to {self.__class__.__name__}![/]", style="bold")
141
+
142
+ def __init__(self, blueprint_id: str, config_path: Optional[Path] = None, enable_terminal_commands: Optional[bool] = None):
143
+ try:
144
+ if not blueprint_id:
145
+ raise ValueError("blueprint_id cannot be empty or None")
146
+ self.blueprint_id = blueprint_id
147
+ self.config_path = config_path # Note: config_path is currently unused if we rely on AppConfig
148
+ self._config: Optional[Dict[str, Any]] = None
149
+ self._llm_profile_name: Optional[str] = None
150
+ self._llm_profile_data: Optional[Dict[str, Any]] = None
151
+ self._markdown_output: bool = True # Default
152
+ # Allow per-instance override
153
+ if enable_terminal_commands is not None:
154
+ self.enable_terminal_commands = enable_terminal_commands
155
+ # Else: use class attribute (default False or set by subclass)
156
+
157
+ logger.info(f"Initializing blueprint '{self.blueprint_id}' (Type: {self.__class__.__name__})")
158
+
159
+ # --- Ensure custom OpenAI client for custom LLM providers ---
160
+ import os
161
+
162
+ # Remove monkey patching and envvar hacks. Always pass config values directly.
163
+ # (Retain only explicit AsyncOpenAI client instantiation in blueprints)
164
+ # (No changes needed here for direct client pattern)
165
+
166
+ self._load_and_process_config()
167
+ except AttributeError as e:
168
+ logger.debug(f"[BlueprintBase.__init__] AttributeError: {e}")
169
+ traceback.print_exc()
170
+ raise
41
171
 
42
172
  def _load_and_process_config(self):
43
- """Loads the main Swarm config and extracts relevant settings."""
173
+ """Loads the main Swarm config and extracts relevant settings. Falls back to empty config if Django unavailable or not found."""
174
+ import os
175
+ import json
176
+ from pathlib import Path
177
+ def redact(val):
178
+ if not isinstance(val, str) or len(val) <= 4:
179
+ return "****"
180
+ return val[:2] + "*" * (len(val)-4) + val[-2:]
181
+ def redact_dict(d):
182
+ if isinstance(d, dict):
183
+ 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()}
184
+ elif isinstance(d, list):
185
+ return [redact_dict(item) for item in d]
186
+ return d
44
187
  try:
45
- # --- Get config from the AppConfig instance ---
46
- app_config_instance = apps.get_app_config('swarm')
47
- # Assuming the loaded config is stored in an attribute named 'config'
48
- # Adjust 'config' if your AppConfig uses a different attribute name
49
- if not hasattr(app_config_instance, 'config') or not app_config_instance.config:
50
- logger.error("Swarm configuration not found on AppConfig instance. Was ready() called?")
51
- raise ValueError("Swarm configuration unavailable via AppConfig.")
52
- self._config = app_config_instance.config
53
- # --- End change ---
54
-
55
- logger.debug(f"Blueprint '{self.blueprint_id}' using loaded Swarm config.")
56
-
57
- # Determine LLM profile
58
- self._llm_profile_name = self._config.get("settings", {}).get("default_llm_profile", "default")
59
- logger.debug(f"Attempting to use LLM profile: '{self._llm_profile_name}'")
60
-
61
- # Get substituted profile data
62
- self._llm_profile_data = get_profile_from_config(self._config, self._llm_profile_name)
63
- logger.info(f"Successfully loaded LLM profile '{self._llm_profile_name}'. Provider: {self._llm_profile_data.get('provider')}")
64
-
65
- # Get markdown setting
188
+ try:
189
+ # --- Get config from the AppConfig instance (Django) ---
190
+ app_config_instance = apps.get_app_config('swarm')
191
+ if not hasattr(app_config_instance, 'config') or not app_config_instance.config:
192
+ raise ValueError("AppConfig for 'swarm' does not have a valid 'config' attribute.")
193
+ config = app_config_instance.config
194
+ logger.debug("Loaded config from Django AppConfig.")
195
+ except Exception as e:
196
+ if _should_debug():
197
+ logger.warning(f"Falling back to CLI/home config due to error: {e}")
198
+ config = None
199
+ # 1. CLI argument (not handled here, handled in cli_handler)
200
+ # 2. Current working directory
201
+ cwd_config = Path.cwd() / "swarm_config.json"
202
+ if cwd_config.exists():
203
+ with open(cwd_config, 'r') as f:
204
+ config = json.load(f)
205
+ # 3. XDG_CONFIG_HOME or ~/.config/swarm/swarm_config.json
206
+ elif os.environ.get("XDG_CONFIG_HOME"):
207
+ xdg_config = Path(os.environ["XDG_CONFIG_HOME"]) / "swarm" / "swarm_config.json"
208
+ if xdg_config.exists():
209
+ with open(xdg_config, 'r') as f:
210
+ config = json.load(f)
211
+ elif (Path.home() / ".config/swarm/swarm_config.json").exists():
212
+ with open(Path.home() / ".config/swarm/swarm_config.json", 'r') as f:
213
+ config = json.load(f)
214
+ # 4. Legacy fallback: ~/.swarm/swarm_config.json
215
+ elif (Path.home() / ".swarm/swarm_config.json").exists():
216
+ with open(Path.home() / ".swarm/swarm_config.json", 'r') as f:
217
+ config = json.load(f)
218
+ # 5. Fallback: OPENAI_API_KEY envvar
219
+ elif os.environ.get("OPENAI_API_KEY"):
220
+ config = {
221
+ "llm": {"default": {"provider": "openai", "model": "gpt-3.5-turbo", "api_key": os.environ["OPENAI_API_KEY"]}},
222
+ "settings": {"default_llm_profile": "default", "default_markdown_output": True},
223
+ "blueprints": {},
224
+ "llm_profile": "default",
225
+ "mcpServers": {}
226
+ }
227
+ logger.info("No config file found, using default config with OPENAI_API_KEY for CLI mode.")
228
+ else:
229
+ config = {}
230
+ 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.")
231
+ if config is not None:
232
+ config = _substitute_env_vars(config)
233
+ self._config = config or {}
234
+
235
+ # --- After config is loaded, set OpenAI client from config if possible ---
236
+ try:
237
+ llm_profiles = self._config.get("llm", {})
238
+ default_profile = llm_profiles.get("default", {})
239
+ base_url = default_profile.get("base_url")
240
+ api_key = default_profile.get("api_key")
241
+ # Expand env vars if present
242
+ import os
243
+ if base_url and base_url.startswith("${"):
244
+ var = base_url[2:-1]
245
+ base_url = os.environ.get(var, base_url)
246
+ if api_key and api_key.startswith("${"):
247
+ var = api_key[2:-1]
248
+ api_key = os.environ.get(var, api_key)
249
+ if base_url and api_key:
250
+ from openai import AsyncOpenAI
251
+ from agents import set_default_openai_client
252
+ _debug_print(f"[DEBUG] (config) Setting OpenAI client: base_url={base_url}, api_key={'set' if api_key else 'NOT SET'}")
253
+ client = AsyncOpenAI(base_url=base_url, api_key=api_key)
254
+ set_default_openai_client(client)
255
+ except Exception as e:
256
+ _debug_print(f"[DEBUG] Failed to set OpenAI client from config: {e}")
257
+
258
+ # --- Debug: Print and log redacted config ---
259
+ redacted_config = redact_dict(self._config)
260
+ logger.debug(f"Loaded config (redacted): {json.dumps(redacted_config, indent=2)}")
261
+
262
+ # --- Process LLM profile name and data ---
263
+ settings_section = self._config.get("settings", {})
264
+ llm_section = self._config.get("llm", {})
265
+ default_profile = settings_section.get("default_llm_profile") or "default"
266
+ self._llm_profile_name = self._config.get("llm_profile") or default_profile
267
+ if "profiles" in llm_section:
268
+ self._llm_profile_data = llm_section["profiles"].get(self._llm_profile_name, {})
269
+ else:
270
+ self._llm_profile_data = llm_section.get(self._llm_profile_name, {})
271
+
66
272
  blueprint_specific_settings = self._config.get("blueprints", {}).get(self.blueprint_id, {})
67
- global_markdown_setting = self._config.get("settings", {}).get("default_markdown_output", True)
273
+ global_markdown_setting = settings_section.get("default_markdown_output", True)
68
274
  self._markdown_output = blueprint_specific_settings.get("markdown_output", global_markdown_setting)
69
275
  logger.debug(f"Markdown output for '{self.blueprint_id}': {self._markdown_output}")
70
276
 
@@ -96,6 +302,19 @@ class BlueprintBase(ABC):
96
302
  raise RuntimeError("LLM profile name accessed before initialization or after failure.")
97
303
  return self._llm_profile_name
98
304
 
305
+ @property
306
+ def slash_commands(self) -> SlashCommandRegistry:
307
+ """Access the global slash command registry. Blueprints can register new commands here."""
308
+ return slash_registry
309
+
310
+ def get_llm_profile(self, profile_name: str) -> dict:
311
+ """Returns the LLM profile dict for the given profile name from config, or empty dict if not found.
312
+ Supports both llm.profiles and direct llm keys for backward compatibility."""
313
+ llm_section = self.config.get("llm", {})
314
+ if "profiles" in llm_section:
315
+ return llm_section["profiles"].get(profile_name, {})
316
+ return llm_section.get(profile_name, {})
317
+
99
318
  @property
100
319
  def should_output_markdown(self) -> bool:
101
320
  """Returns whether the blueprint should format output as Markdown."""
@@ -106,6 +325,9 @@ class BlueprintBase(ABC):
106
325
  """
107
326
  The main execution method for the blueprint.
108
327
  """
328
+ import os
329
+ import pprint
330
+ logger.debug("ENVIRONMENT DUMP BEFORE MODEL CALL:")
331
+ pprint.pprint(dict(os.environ))
109
332
  raise NotImplementedError("Subclasses must implement the 'run' method.")
110
333
  yield {}
111
-
@@ -54,35 +54,38 @@ def discover_blueprints(blueprint_dir: str) -> Dict[str, Type[BlueprintBase]]:
54
54
  return blueprints
55
55
 
56
56
  # Iterate over items inside the base blueprint directory
57
- for item_name in os.listdir(base_dir):
58
- item_path = base_dir / item_name
59
-
60
- if not item_path.is_dir():
57
+ for subdir in base_dir.iterdir():
58
+ if not subdir.is_dir():
61
59
  continue # Skip files directly under blueprints/
62
60
 
63
61
  # Use directory name as blueprint name (e.g., 'echocraft')
64
- blueprint_name = item_name
65
- logger.debug(f"Processing potential blueprint '{blueprint_name}' in directory: {item_name}")
62
+ blueprint_name = subdir.name
63
+ logger.debug(f"Processing potential blueprint '{blueprint_name}' in directory: {subdir.name}")
66
64
 
67
65
  # Look for the specific .py file, e.g., blueprint_echocraft.py
68
66
  py_file_name = f"blueprint_{blueprint_name}.py"
69
- py_file_path = item_path / py_file_name
67
+ py_file_path = subdir / py_file_name
70
68
 
71
69
  if not py_file_path.is_file():
72
70
  # Also check for just {blueprint_name}.py if that's a convention
73
71
  alt_py_file_name = f"{blueprint_name}.py"
74
- alt_py_file_path = item_path / alt_py_file_name
72
+ alt_py_file_path = subdir / alt_py_file_name
75
73
  if alt_py_file_path.is_file():
76
74
  py_file_path = alt_py_file_path # Use the alternative path
77
75
  py_file_name = alt_py_file_name
78
76
  logger.debug(f"Found alternative blueprint file: {py_file_name}")
79
77
  else:
80
- logger.warning(f"Skipping directory '{item_name}': Neither '{py_file_name}' nor '{alt_py_file_name}' found.")
78
+ logger.warning(f"Skipping directory '{subdir.name}': Neither '{py_file_name}' nor '{alt_py_file_name}' found.")
81
79
  continue
82
80
 
83
81
 
84
82
  # Construct module import path, e.g., blueprints.echocraft.blueprint_echocraft
85
- module_import_path = f"{base_dir.name}.{item_name}.{py_file_path.stem}"
83
+ if py_file_path.name.startswith('blueprint_gatcha'):
84
+ module_import_path = f"swarm.blueprints.gatcha.{py_file_path.stem}"
85
+ elif py_file_path.name.startswith('blueprint_'):
86
+ module_import_path = f"swarm.blueprints.{subdir.name}.{py_file_path.stem}"
87
+ else:
88
+ continue
86
89
 
87
90
  try:
88
91
  # Ensure parent directory is in path
@@ -123,4 +126,3 @@ def discover_blueprints(blueprint_dir: str) -> Dict[str, Type[BlueprintBase]]:
123
126
 
124
127
  logger.info(f"Blueprint discovery complete. Found: {list(blueprints.keys())}")
125
128
  return blueprints
126
-
@@ -2,8 +2,10 @@ import argparse
2
2
  import asyncio
3
3
  import json
4
4
  import logging
5
+ import os
5
6
  import signal
6
7
  import sys
8
+ from dotenv import load_dotenv
7
9
  from pathlib import Path
8
10
  from typing import Any, Dict, Optional, Type
9
11
 
@@ -14,6 +16,19 @@ if TYPE_CHECKING:
14
16
 
15
17
  logger = logging.getLogger("swarm.cli")
16
18
 
19
+ # --- DEBUG PRINTS REMOVED BY CASCADE ---
20
+ # print(f"[DEBUG] CLI handler startup: sys.argv={sys.argv}")
21
+ # print(f"[DEBUG] CLI handler startup: LITELLM_MODEL={os.environ.get('LITELLM_MODEL')}, DEFAULT_LLM={os.environ.get('DEFAULT_LLM')}")
22
+
23
+ # --- FORCE LOAD .env EARLY for CLI/LLM ---
24
+ project_root = Path(__file__).parent.parent.parent.parent # /home/chatgpt/open-swarm
25
+ dotenv_path = project_root / ".env"
26
+ load_dotenv(dotenv_path=dotenv_path, override=True)
27
+ # print(f"[DEBUG] Loaded .env from: {dotenv_path}")
28
+ # print(f"[DEBUG] LITELLM_MODEL={os.environ.get('LITELLM_MODEL')}")
29
+ # print(f"[DEBUG] LITELLM_BASE_URL={os.environ.get('LITELLM_BASE_URL')}")
30
+ # print(f"[DEBUG] LITELLM_API_KEY={'set' if os.environ.get('LITELLM_API_KEY') else 'NOT SET'}")
31
+
17
32
  async def _run_blueprint_async_with_shutdown(blueprint: 'BlueprintBase', instruction: str):
18
33
  """Runs the blueprint's async method and handles graceful shutdown."""
19
34
  loop = asyncio.get_running_loop()
@@ -40,55 +55,20 @@ async def _run_blueprint_async_with_shutdown(blueprint: 'BlueprintBase', instruc
40
55
  logger.error(f"Unexpected error setting fallback signal handler for {sig.name}: {e}", exc_info=True)
41
56
 
42
57
 
43
- # Wrap the main execution in a task to allow cancellation
44
- main_task = loop.create_task(blueprint._run_non_interactive(instruction), name=f"BlueprintRun_{blueprint.__class__.__name__}")
45
-
46
- # Wait for either the main task or the stop event
47
- done, pending = await asyncio.wait(
48
- [main_task, loop.create_task(stop_event.wait(), name="ShutdownWatcher")],
49
- return_when=asyncio.FIRST_COMPLETED
50
- )
58
+ # Instead of wrapping in a task and awaiting, use async for to support async generators
59
+ try:
60
+ async for chunk in blueprint._run_non_interactive(instruction):
61
+ # Print the full JSON chunk
62
+ print(json.dumps(chunk, ensure_ascii=False))
63
+ # If chunk contains 'messages', print each assistant message's content for CLI/test UX
64
+ if isinstance(chunk, dict) and 'messages' in chunk:
65
+ for msg in chunk['messages']:
66
+ if msg.get('role') == 'assistant' and 'content' in msg:
67
+ print(msg['content'])
68
+ except Exception as e:
69
+ logger.critical(f"Blueprint execution failed with unhandled exception: {e}", exc_info=True)
70
+ sys.exit(1)
51
71
 
52
- # Cleanup signal handlers after wait returns
53
- for sig in (signal.SIGINT, signal.SIGTERM):
54
- try:
55
- loop.remove_signal_handler(sig)
56
- except NotImplementedError:
57
- try:
58
- signal.signal(sig, signal.SIG_DFL) # Restore default handler
59
- except Exception:
60
- pass # Ignore errors during cleanup
61
-
62
- # Check if the stop event was triggered
63
- if stop_event.is_set():
64
- logger.warning("Graceful shutdown initiated. Cancelling main task...")
65
- if not main_task.done():
66
- main_task.cancel()
67
- try:
68
- # Wait briefly for cancellation to propagate and cleanup within the task
69
- await asyncio.wait_for(main_task, timeout=10.0) # Increased timeout slightly
70
- except asyncio.CancelledError:
71
- logger.info("Main task successfully cancelled.")
72
- except asyncio.TimeoutError:
73
- logger.error("Main task did not cancel within timeout. Potential resource leak.")
74
- except Exception as e:
75
- logger.error(f"Error during task cancellation waiting: {e}", exc_info=True)
76
- else:
77
- logger.info("Main task already completed before cancellation request.")
78
- # The _run_non_interactive's AsyncExitStack should handle MCP cleanup
79
- else:
80
- # If the main task finished first, check for exceptions
81
- if main_task in done:
82
- try:
83
- main_task.result() # Raise exception if one occurred in the task
84
- logger.debug("Main task completed successfully.")
85
- except asyncio.CancelledError:
86
- logger.info("Main task was cancelled externally (unexpected).")
87
- except Exception as e:
88
- # Error should have been logged within _run_non_interactive
89
- # We exit here because the main operation failed
90
- logger.critical(f"Blueprint execution failed with unhandled exception: {e}", exc_info=True)
91
- sys.exit(1) # Exit with error status if task failed
92
72
 
93
73
 
94
74
  def run_blueprint_cli(
@@ -149,14 +129,13 @@ def run_blueprint_cli(
149
129
  # --- Instantiate and Run Blueprint ---
150
130
  blueprint_instance: Optional['BlueprintBase'] = None
151
131
  try:
132
+ # Always provide a blueprint_id (use class name if not supplied by CLI args)
133
+ blueprint_id = getattr(args, 'blueprint_id', None) or getattr(blueprint_cls, 'DEFAULT_BLUEPRINT_ID', None) or blueprint_cls.__name__
152
134
  # Instantiate the blueprint, passing necessary config/flags
153
135
  blueprint_instance = blueprint_cls(
154
- config_path_override=args.config_path,
155
- profile_override=args.profile,
156
- config_overrides=cli_config_overrides,
157
- debug=args.debug,
158
- quiet=args.quiet,
159
- force_markdown=args.markdown,
136
+ blueprint_id,
137
+ config_path=args.config_path,
138
+
160
139
  # Pass necessary context if needed by __init__
161
140
  # default_config_path=default_config_path,
162
141
  # swarm_version=swarm_version
@@ -182,4 +161,3 @@ def run_blueprint_cli(
182
161
  finally:
183
162
  logger.debug("Blueprint CLI execution finished.")
184
163
  # Any final cleanup outside the async loop (rarely needed here)
185
-
@@ -4,6 +4,7 @@ Output utilities for Swarm blueprints.
4
4
 
5
5
  import json
6
6
  import logging
7
+ import os
7
8
  import sys
8
9
  from typing import List, Dict, Any
9
10
 
@@ -11,6 +12,9 @@ from typing import List, Dict, Any
11
12
  try:
12
13
  from rich.markdown import Markdown
13
14
  from rich.console import Console
15
+ from rich.panel import Panel
16
+ from rich.text import Text
17
+ from rich.rule import Rule
14
18
  RICH_AVAILABLE = True
15
19
  except ImportError:
16
20
  RICH_AVAILABLE = False
@@ -28,6 +32,44 @@ def render_markdown(content: str) -> None:
28
32
  md = Markdown(content)
29
33
  console.print(md) # Rich handles flushing
30
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
+
31
73
  def pretty_print_response(messages: List[Dict[str, Any]], use_markdown: bool = False, spinner=None) -> None:
32
74
  """Format and print messages, optionally rendering assistant content as markdown."""
33
75
  # --- DEBUG PRINT ---
@@ -92,4 +134,40 @@ def pretty_print_response(messages: List[Dict[str, Any]], use_markdown: bool = F
92
134
  # --- DEBUG PRINT ---
93
135
  print(f"[DEBUG Skipping message {i} with role '{role}']", flush=True)
94
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
95
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)
@@ -6,29 +6,36 @@ import os
6
6
  import sys
7
7
  import threading
8
8
  import time
9
+ from typing import Optional
9
10
 
10
11
  class Spinner:
11
12
  """Simple terminal spinner for interactive feedback."""
12
13
  # Define spinner characters (can be customized)
13
14
  SPINNER_CHARS = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
14
- # SPINNER_CHARS = ['|', '/', '-', '\\'] # Simpler alternative
15
+ # Custom status sequences for special cases
16
+ STATUS_SEQUENCES = {
17
+ 'generating': ['Generating.', 'Generating..', 'Generating...'],
18
+ 'running': ['Running...']
19
+ }
15
20
 
16
- def __init__(self, interactive: bool):
21
+ def __init__(self, interactive: bool, custom_sequence: str = None):
17
22
  """
18
23
  Initialize the spinner.
19
24
 
20
25
  Args:
21
26
  interactive (bool): Hint whether the environment is interactive.
22
27
  Spinner is disabled if False or if output is not a TTY.
28
+ custom_sequence (str): Optional name for a custom status sequence (e.g., 'generating', 'running').
23
29
  """
24
30
  self.interactive = interactive
25
- # Check if output is a TTY (terminal) and interactive flag is True
26
31
  self.is_tty = sys.stdout.isatty()
27
32
  self.enabled = self.interactive and self.is_tty
28
33
  self.running = False
29
34
  self.thread: Optional[threading.Thread] = None
30
35
  self.status = ""
31
36
  self.index = 0
37
+ self.custom_sequence = custom_sequence
38
+ self.sequence_idx = 0
32
39
 
33
40
  def start(self, status: str = "Processing..."):
34
41
  """Start the spinner with an optional status message."""
@@ -36,7 +43,7 @@ class Spinner:
36
43
  return # Do nothing if disabled or already running
37
44
  self.status = status
38
45
  self.running = True
39
- # Run the spinner animation in a separate daemon thread
46
+ self.sequence_idx = 0
40
47
  self.thread = threading.Thread(target=self._spin, daemon=True)
41
48
  self.thread.start()
42
49
 
@@ -47,30 +54,33 @@ class Spinner:
47
54
  self.running = False
48
55
  if self.thread is not None:
49
56
  self.thread.join() # Wait for the thread to finish
50
- # Clear the spinner line using ANSI escape codes
51
- # \r: Carriage return (move cursor to beginning of line)
52
- # \033[K: Clear line from cursor to end
53
57
  sys.stdout.write("\r\033[K")
54
58
  sys.stdout.flush()
55
- self.thread = None # Reset thread
59
+ self.thread = None
56
60
 
57
61
  def _spin(self):
58
62
  """Internal method running in the spinner thread to animate."""
63
+ start_time = time.time()
64
+ warned = False
59
65
  while self.running:
60
- # Get the next spinner character
61
- char = self.SPINNER_CHARS[self.index % len(self.SPINNER_CHARS)]
62
- # Write spinner char and status, overwrite previous line content
63
- try:
64
- # \r moves cursor to beginning, \033[K clears the rest of the line
66
+ elapsed = time.time() - start_time
67
+ if self.custom_sequence and self.custom_sequence in self.STATUS_SEQUENCES:
68
+ seq = self.STATUS_SEQUENCES[self.custom_sequence]
69
+ # If taking longer than 10s, show special message
70
+ if elapsed > 10 and not warned:
71
+ msg = f"{seq[-1]} Taking longer than expected"
72
+ warned = True
73
+ else:
74
+ msg = seq[self.sequence_idx % len(seq)]
75
+ sys.stdout.write(f"\r{msg}\033[K")
76
+ sys.stdout.flush()
77
+ self.sequence_idx += 1
78
+ else:
79
+ char = self.SPINNER_CHARS[self.index % len(self.SPINNER_CHARS)]
65
80
  sys.stdout.write(f"\r{char} {self.status}\033[K")
66
81
  sys.stdout.flush()
67
- except BlockingIOError:
68
- # Handle potential issues if stdout is blocked (less likely for TTY)
69
- time.sleep(0.1)
70
- continue
71
- self.index += 1
72
- # Pause for animation effect
73
- time.sleep(0.1)
82
+ self.index += 1
83
+ time.sleep(0.4 if self.custom_sequence else 0.1)
74
84
 
75
85
  # Example usage (if run directly)
76
86
  if __name__ == "__main__":
@@ -88,4 +98,3 @@ if __name__ == "__main__":
88
98
  finally:
89
99
  s.stop() # Ensure spinner stops on exit/error
90
100
  print("Test finished.")
91
-
@@ -1,8 +1,14 @@
1
1
  # src/swarm/extensions/blueprint/modes/cli_mode/cli_args.py
2
2
 
3
3
  import argparse
4
+ import os
5
+ import sys
4
6
  from typing import Namespace
5
7
 
8
+ # --- DEBUG PRINTS REMOVED BY CASCADE ---
9
+ # print(f"[DEBUG] cli_args.py startup: sys.argv={sys.argv}")
10
+ # print(f"[DEBUG] cli_args.py startup: LITELLM_MODEL={os.environ.get('LITELLM_MODEL')}, DEFAULT_LLM={os.environ.get('DEFAULT_LLM')}")
11
+
6
12
  def parse_arguments() -> Namespace:
7
13
  """
8
14
  Parse command-line arguments for dynamic LLM configuration, MCP server management, and other overrides.
@@ -57,6 +57,10 @@ def _substitute_env_vars_recursive(data: Any) -> Any:
57
57
  if isinstance(data,str): return os.path.expandvars(data)
58
58
  return data
59
59
 
60
+ def _substitute_env_vars(data: Any) -> Any:
61
+ """Public API: Recursively substitute environment variables in dict, list, str."""
62
+ return _substitute_env_vars_recursive(data)
63
+
60
64
  def create_default_config(config_path: Path):
61
65
  """Creates a default configuration file with valid JSON."""
62
66
  default_config = {
@@ -88,4 +92,3 @@ def create_default_config(config_path: Path):
88
92
  except Exception as e:
89
93
  logger.error(f"Failed to create default config file at {config_path}: {e}", exc_info=True)
90
94
  raise
91
-
@@ -18,7 +18,6 @@ from ..utils.redact import redact_sensitive_data
18
18
  from ..utils.general_utils import serialize_datetime
19
19
  from ..utils.message_utils import filter_duplicate_system_messages, update_null_content
20
20
  from ..utils.context_utils import get_token_count, truncate_message_history
21
- # --- REMOVED import: from ..utils.message_sequence import repair_message_payload ---
22
21
 
23
22
  # Configure module-level logging
24
23
  logger = logging.getLogger(__name__)
@@ -29,6 +28,29 @@ if not logger.handlers:
29
28
  stream_handler.setFormatter(formatter)
30
29
  logger.addHandler(stream_handler)
31
30
 
31
+ # --- PATCH: Suppress OpenAI tracing/telemetry errors if using LiteLLM/custom endpoint ---
32
+ import logging
33
+ import os
34
+ if os.environ.get("LITELLM_BASE_URL") or os.environ.get("OPENAI_BASE_URL"):
35
+ # Silence openai.agents tracing/telemetry errors
36
+ logging.getLogger("openai.agents").setLevel(logging.CRITICAL)
37
+ try:
38
+ import openai.agents.tracing
39
+ openai.agents.tracing.TracingClient = lambda *a, **kw: None
40
+ except Exception:
41
+ pass
42
+
43
+ # --- PATCH: Enforce custom endpoint, never fallback to OpenAI if custom base_url is set ---
44
+ def _enforce_litellm_only(client):
45
+ # If client has a base_url attribute, check it
46
+ base_url = getattr(client, 'base_url', None)
47
+ if base_url and 'openai.com' in base_url:
48
+ return # Using OpenAI, allowed
49
+ if base_url and 'openai.com' not in base_url:
50
+ # If any fallback to OpenAI API is attempted, raise error
51
+ import traceback
52
+ raise RuntimeError(f"Attempted fallback to OpenAI API when custom base_url is set! base_url={base_url}\n{traceback.format_stack()}")
53
+
32
54
 
33
55
  async def get_chat_completion(
34
56
  client: AsyncOpenAI,
@@ -44,6 +66,7 @@ async def get_chat_completion(
44
66
  stream: bool = False,
45
67
  debug: bool = False
46
68
  ) -> Union[Dict[str, Any], AsyncGenerator[Any, None]]:
69
+ _enforce_litellm_only(client)
47
70
  """
48
71
  Retrieve a chat completion from the LLM for the given agent and history.
49
72
  Relies on openai-agents Runner for actual execution, this might become deprecated.
@@ -62,6 +85,12 @@ async def get_chat_completion(
62
85
  redacted_kwargs = redact_sensitive_data(client_kwargs, sensitive_keys=["api_key"])
63
86
  logger.debug(f"Using client with model='{active_model}', base_url='{client_kwargs.get('base_url', 'default')}', api_key={redacted_kwargs['api_key']}")
64
87
 
88
+ # --- ENFORCE: Disallow fallback to OpenAI if custom base_url is set ---
89
+ if client_kwargs.get("base_url") and "openai.com" not in client_kwargs["base_url"]:
90
+ # If the base_url is set and is not OpenAI, ensure no fallback to OpenAI API
91
+ if "openai.com" in os.environ.get("OPENAI_API_BASE", ""):
92
+ raise RuntimeError(f"[SECURITY] Fallback to OpenAI API attempted with base_url={client_kwargs['base_url']}. Refusing for safety.")
93
+
65
94
  context_variables = defaultdict(str, context_variables)
66
95
  instructions = agent.instructions(context_variables) if callable(agent.instructions) else agent.instructions
67
96
  if not isinstance(instructions, str):
@@ -152,6 +181,7 @@ async def get_chat_completion_message(
152
181
  stream: bool = False,
153
182
  debug: bool = False
154
183
  ) -> Union[Dict[str, Any], AsyncGenerator[Any, None]]:
184
+ _enforce_litellm_only(client)
155
185
  """
156
186
  Wrapper to retrieve and validate a chat completion message (returns dict or stream).
157
187
  Relies on openai-agents Runner for actual execution, this might become deprecated.
swarm/settings.py CHANGED
@@ -12,7 +12,7 @@ BASE_DIR = Path(__file__).resolve().parent.parent # Points to src/
12
12
  # --- Load .env file ---
13
13
  dotenv_path = BASE_DIR.parent / '.env'
14
14
  load_dotenv(dotenv_path=dotenv_path)
15
- print(f"[Settings] Attempted to load .env from: {dotenv_path}")
15
+ # print(f"[Settings] Attempted to load .env from: {dotenv_path}")
16
16
  # ---
17
17
 
18
18
  SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'django-insecure-fallback-key-for-dev')
@@ -30,12 +30,12 @@ SWARM_API_KEY = _raw_api_token # Assign the loaded token (or None)
30
30
  if ENABLE_API_AUTH:
31
31
  # Add assertion to satisfy type checkers within this block
32
32
  assert SWARM_API_KEY is not None, "SWARM_API_KEY cannot be None when ENABLE_API_AUTH is True"
33
- print(f"[Settings] SWARM_API_KEY loaded: {SWARM_API_KEY[:4]}...{SWARM_API_KEY[-4:]}")
34
- print("[Settings] ENABLE_API_AUTH is True.")
33
+ # print(f"[Settings] SWARM_API_KEY loaded: {SWARM_API_KEY[:4]}...{SWARM_API_KEY[-4:]}")
34
+ # print("[Settings] ENABLE_API_AUTH is True.")
35
35
  else:
36
- print("[Settings] API_AUTH_TOKEN env var not set. SWARM_API_KEY is None.")
37
- print("[Settings] ENABLE_API_AUTH is False.")
38
-
36
+ # print("[Settings] API_AUTH_TOKEN env var not set. SWARM_API_KEY is None.")
37
+ # print("[Settings] ENABLE_API_AUTH is False.")
38
+ pass
39
39
 
40
40
  SWARM_CONFIG_PATH = os.getenv('SWARM_CONFIG_PATH', str(BASE_DIR.parent / 'swarm_config.json'))
41
41
  BLUEPRINT_DIRECTORY = os.getenv('BLUEPRINT_DIRECTORY', str(BASE_DIR / 'swarm' / 'blueprints'))
@@ -175,4 +175,3 @@ LOGIN_URL = '/login/'
175
175
  LOGIN_REDIRECT_URL = '/'
176
176
  LOGOUT_REDIRECT_URL = '/'
177
177
  CSRF_TRUSTED_ORIGINS = os.getenv('DJANGO_CSRF_TRUSTED_ORIGINS', 'http://localhost:8000,http://127.0.0.1:8000').split(',')
178
-