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.
- {open_swarm-0.1.1744936234.dist-info → open_swarm-0.1.1744936333.dist-info}/METADATA +1 -1
- {open_swarm-0.1.1744936234.dist-info → open_swarm-0.1.1744936333.dist-info}/RECORD +16 -16
- {open_swarm-0.1.1744936234.dist-info → open_swarm-0.1.1744936333.dist-info}/entry_points.txt +1 -0
- swarm/consumers.py +19 -0
- swarm/extensions/blueprint/agent_utils.py +1 -1
- swarm/extensions/blueprint/blueprint_base.py +265 -43
- swarm/extensions/blueprint/blueprint_discovery.py +13 -11
- swarm/extensions/blueprint/cli_handler.py +33 -55
- swarm/extensions/blueprint/output_utils.py +78 -0
- swarm/extensions/blueprint/spinner.py +30 -21
- swarm/extensions/cli/cli_args.py +6 -0
- swarm/extensions/config/config_loader.py +4 -1
- swarm/llm/chat_completion.py +31 -1
- swarm/settings.py +6 -7
- {open_swarm-0.1.1744936234.dist-info → open_swarm-0.1.1744936333.dist-info}/WHEEL +0 -0
- {open_swarm-0.1.1744936234.dist-info → open_swarm-0.1.1744936333.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.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=
|
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=
|
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=
|
54
|
-
swarm/extensions/blueprint/blueprint_base.py,sha256=
|
55
|
-
swarm/extensions/blueprint/blueprint_discovery.py,sha256=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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.
|
256
|
-
open_swarm-0.1.
|
257
|
-
open_swarm-0.1.
|
258
|
-
open_swarm-0.1.
|
259
|
-
open_swarm-0.1.
|
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,,
|
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
|
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
|
-
|
22
|
-
"""
|
23
|
-
Initializes the blueprint.
|
123
|
+
enable_terminal_commands: bool = False # By default, terminal command execution is disabled
|
24
124
|
|
25
|
-
|
26
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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 =
|
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
|
58
|
-
|
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 =
|
65
|
-
logger.debug(f"Processing potential blueprint '{blueprint_name}' in directory: {
|
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 =
|
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 =
|
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 '{
|
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
|
-
|
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
|
-
#
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
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
|
-
|
155
|
-
|
156
|
-
|
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
|
-
#
|
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
|
-
|
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
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
68
|
-
|
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
|
-
|
swarm/extensions/cli/cli_args.py
CHANGED
@@ -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
|
-
|
swarm/llm/chat_completion.py
CHANGED
@@ -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
|
-
|
File without changes
|
{open_swarm-0.1.1744936234.dist-info → open_swarm-0.1.1744936333.dist-info}/licenses/LICENSE
RENAMED
File without changes
|