open-swarm 0.1.1743372974__py3-none-any.whl → 0.1.1743449197__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.1743372974
3
+ Version: 0.1.1743449197
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
@@ -3,17 +3,18 @@ swarm/apps.py,sha256=up4C3m2JeyXeUcH-wYeReCuiOBVJ6404w9OfaRChLwM,2568
3
3
  swarm/auth.py,sha256=8JIk1VbBvFFwOijEJAsrx6si802ZSMGnErXvmo0izUg,5935
4
4
  swarm/consumers.py,sha256=wESLamkhbi4SEZt9k3yx6eU9ufOIZMCAL-OAXjJBGXE,5056
5
5
  swarm/messages.py,sha256=CwADrjlj-uVmm-so1xIZvN1UkEWdzSn_hu7slfhuS8w,6549
6
+ swarm/middleware.py,sha256=lPlHbFg9Rm9lUuvg026d4zTDjRMc8bQi0JegpGdqIZQ,3198
6
7
  swarm/models.py,sha256=Ix0WEYYqza2lbOEBNesikRCs3XGUPWmqQyMWzZYUaxM,1494
7
8
  swarm/permissions.py,sha256=iM86fSL1TtgqJzgDkS3Dl82X6Xk7VDHWwdBDfs5RKWc,1671
8
9
  swarm/serializers.py,sha256=4g3G2FdWpSIuLLC_SBKoNITw1b0G83Bxo7YHc-kjsro,4550
9
- swarm/settings.py,sha256=rGPcnLSf6WetpeovBb6AhDzWi2GwaFbsQ8UZjRVTMtI,6334
10
+ swarm/settings.py,sha256=wrQoWfNylY_54z5c54x0TLe2Q9KEqvawNXjqCVhWuyI,6616
10
11
  swarm/tool_executor.py,sha256=KHM2mTGgbbTgWNN3fbV5c4MDY238OTLwaaqtkczFHFQ,12385
11
12
  swarm/urls.py,sha256=9eRQWsB-Vs3Nmes4mtlZtk_Rvuixf4Y9uwrX9dVQ9Is,3292
12
13
  swarm/util.py,sha256=G4x2hXopHhB7IdGCkUXGoykYWyiICnjxg7wcr-WqL8I,4644
13
14
  swarm/wsgi.py,sha256=REM_u4HpMCkO0ddrOUXgtY-ITL-VTbRB1-WHvFJAtAU,408
14
15
  swarm/agent/__init__.py,sha256=YESGu_UXEBxrlQwghodUMN0vmXZDwWMU7DclCUvoklA,104
15
16
  swarm/blueprints/README.md,sha256=tsngbSB9N0tILcz_m1OGAjyKZQYlGTN-i5e5asq1GbE,8478
16
- swarm/blueprints/burnt_noodles/blueprint_burnt_noodles.py,sha256=LwKVYTRRg6ceNpvbuzM-bl7NukBGNu3bOU9Q3h6WjOU,22834
17
+ swarm/blueprints/burnt_noodles/blueprint_burnt_noodles.py,sha256=vopDlBjVUNeSq6WItdkmtWJfObgbtL6wNAy2njsskkY,19607
17
18
  swarm/blueprints/chatbot/blueprint_chatbot.py,sha256=D31OgSoxllhzn8f7cdVYflqzadN2SZ61rabPuK6EqQ8,4728
18
19
  swarm/blueprints/chatbot/templates/chatbot/chatbot.html,sha256=REFnqNg0EHsXxAUfaCJe1YgOKiV_umBXuC6y8veF5CU,1568
19
20
  swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py,sha256=JK_rmZgPMw4PdQFrMverrwgcjH0NRkuqkchYOJwXYuM,9809
@@ -62,6 +63,7 @@ swarm/extensions/blueprint/config_loader.py,sha256=ldQGtv4tXeDJzL2GCylDxykZxYBo4
62
63
  swarm/extensions/blueprint/django_utils.py,sha256=ObtkmF1JW4H2OEYa7vC6ussUsMBtDsZTTVeHGHI-GOQ,17457
63
64
  swarm/extensions/blueprint/interactive_mode.py,sha256=vGmMuAgC93TLjMi2RkXQ2FkWfIUblyOTFGHmVdGKLSQ,4572
64
65
  swarm/extensions/blueprint/output_utils.py,sha256=8OtVE3gEvPeeTu4Juo6Ad6omSlMqSuAtckXXx7P1CyQ,4022
66
+ swarm/extensions/blueprint/runnable_blueprint.py,sha256=1MywZ54vUysLVtYmwCbcDYQmQnoZffCHgsArbe-VKe8,1813
65
67
  swarm/extensions/blueprint/spinner.py,sha256=3J0ZrNzoI5O5qR7hnCeRM3dZx2fLb_H3zkoj_AYt5LQ,3394
66
68
  swarm/extensions/blueprint/modes/rest_mode.py,sha256=KZuB_j2NfomER7CmlsLBqRipU3DymKY-9RpoGilMH0I,1357
67
69
  swarm/extensions/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -253,8 +255,8 @@ swarm/views/message_views.py,sha256=sDUnXyqKXC8WwIIMAlWf00s2_a2T9c75Na5FvYMJwBM,
253
255
  swarm/views/model_views.py,sha256=aAbU4AZmrOTaPeKMWtoKK7FPYHdaN3Zbx55JfKzYTRY,2937
254
256
  swarm/views/utils.py,sha256=geX3Z5ZDKFYyXYBMilc-4qgOSjhujK3AfRtvbXgFpXk,3643
255
257
  swarm/views/web_views.py,sha256=ExQQeJpZ8CkLZQC_pXKOOmdnEy2qR3wEBP4LLp27DPU,7404
256
- open_swarm-0.1.1743372974.dist-info/METADATA,sha256=blPlP9G_zGz6Vi-7nxiIpWiegGOvObE1FCYtDJJf3aw,13678
257
- open_swarm-0.1.1743372974.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
258
- open_swarm-0.1.1743372974.dist-info/entry_points.txt,sha256=LudR3dBqGnglrE1n2zAeWo38Ck0m57sKPW36KfK-pzo,71
259
- open_swarm-0.1.1743372974.dist-info/licenses/LICENSE,sha256=BU9bwRlnOt_JDIb6OT55Q4leLZx9RArDLTFnlDIrBEI,1062
260
- open_swarm-0.1.1743372974.dist-info/RECORD,,
258
+ open_swarm-0.1.1743449197.dist-info/METADATA,sha256=dtlfI53Sbe7oMT0WtK01-I0kyIpDCz8OXr8KWeU1_dc,13678
259
+ open_swarm-0.1.1743449197.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
260
+ open_swarm-0.1.1743449197.dist-info/entry_points.txt,sha256=z1UIVRRhri-V-hWxFkDEYu0SZPUIsVO4KpDaodgcFzU,125
261
+ open_swarm-0.1.1743449197.dist-info/licenses/LICENSE,sha256=BU9bwRlnOt_JDIb6OT55Q4leLZx9RArDLTFnlDIrBEI,1062
262
+ open_swarm-0.1.1743449197.dist-info/RECORD,,
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
+ swarm-api = swarm.extensions.launchers.swarm_api:main
2
3
  swarm-cli = swarm.extensions.launchers.swarm_cli:app
@@ -7,7 +7,7 @@ import shlex # Added for safe command splitting
7
7
  import re
8
8
  import inspect
9
9
  from pathlib import Path # Use pathlib for better path handling
10
- from typing import Dict, Any, List, Optional, ClassVar
10
+ from typing import Dict, Any, List, Optional, ClassVar, AsyncGenerator
11
11
 
12
12
  try:
13
13
  # Core imports from openai-agents
@@ -30,192 +30,111 @@ except ImportError as e:
30
30
  logger = logging.getLogger(__name__)
31
31
  # Logging level is controlled by BlueprintBase based on --debug flag
32
32
 
33
- # --- Tool Definitions ---
34
- # Standalone functions decorated as tools for git and testing operations.
35
- # Enhanced error handling and logging added.
36
-
37
- @function_tool
38
- def git_status() -> str:
33
+ # --- Tool Logic Definitions (Undecorated) ---
34
+ def _git_status_logic() -> str:
39
35
  """Executes 'git status --porcelain' and returns the current repository status."""
40
- logger.info("Executing git status --porcelain") # Keep INFO for tool execution start
36
+ logger.info("Executing git status --porcelain")
41
37
  try:
42
- # Using --porcelain for machine-readable output
43
38
  result = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True, check=True, timeout=30)
44
39
  output = result.stdout.strip()
45
40
  logger.debug(f"Git status raw output:\n{output}")
46
41
  return f"OK: Git Status:\n{output}" if output else "OK: No changes detected in the working directory."
47
- except FileNotFoundError:
48
- logger.error("Git command not found. Is git installed and in PATH?")
49
- return "Error: git command not found."
50
- except subprocess.CalledProcessError as e:
51
- logger.error(f"Error executing git status: {e.stderr}")
52
- return f"Error executing git status: {e.stderr}"
53
- except subprocess.TimeoutExpired:
54
- logger.error("Git status command timed out.")
55
- return "Error: Git status command timed out."
56
- except Exception as e:
57
- logger.error(f"Unexpected error during git status: {e}", exc_info=logger.level <= logging.DEBUG)
58
- return f"Error during git status: {e}"
42
+ except FileNotFoundError: logger.error("Git command not found."); return "Error: git command not found."
43
+ except subprocess.CalledProcessError as e: logger.error(f"Error executing git status: {e.stderr}"); return f"Error executing git status: {e.stderr}"
44
+ except subprocess.TimeoutExpired: logger.error("Git status command timed out."); return "Error: Git status command timed out."
45
+ except Exception as e: logger.error(f"Unexpected error during git status: {e}", exc_info=logger.level <= logging.DEBUG); return f"Error during git status: {e}"
59
46
 
60
- @function_tool
61
- def git_diff() -> str:
47
+ def _git_diff_logic() -> str:
62
48
  """Executes 'git diff' and returns the differences in the working directory."""
63
- logger.info("Executing git diff") # Keep INFO for tool execution start
49
+ logger.info("Executing git diff")
64
50
  try:
65
- result = subprocess.run(["git", "diff"], capture_output=True, text=True, check=False, timeout=30) # Use check=False, handle exit code
66
- output = result.stdout
67
- stderr = result.stderr.strip()
68
- if result.returncode != 0 and stderr: # Error occurred
69
- logger.error(f"Error executing git diff (Exit Code {result.returncode}): {stderr}")
70
- return f"Error executing git diff: {stderr}"
71
- logger.debug(f"Git diff raw output (Exit Code {result.returncode}):\n{output[:1000]}...") # Log snippet
51
+ result = subprocess.run(["git", "diff"], capture_output=True, text=True, check=False, timeout=30)
52
+ output = result.stdout; stderr = result.stderr.strip()
53
+ if result.returncode != 0 and stderr: logger.error(f"Error executing git diff (Exit Code {result.returncode}): {stderr}"); return f"Error executing git diff: {stderr}"
54
+ logger.debug(f"Git diff raw output (Exit Code {result.returncode}):\n{output[:1000]}...")
72
55
  return f"OK: Git Diff Output:\n{output}" if output else "OK: No differences found."
73
- except FileNotFoundError:
74
- logger.error("Git command not found.")
75
- return "Error: git command not found."
76
- except subprocess.TimeoutExpired:
77
- logger.error("Git diff command timed out.")
78
- return "Error: Git diff command timed out."
79
- except Exception as e:
80
- logger.error(f"Unexpected error during git diff: {e}", exc_info=logger.level <= logging.DEBUG)
81
- return f"Error during git diff: {e}"
56
+ except FileNotFoundError: logger.error("Git command not found."); return "Error: git command not found."
57
+ except subprocess.TimeoutExpired: logger.error("Git diff command timed out."); return "Error: Git diff command timed out."
58
+ except Exception as e: logger.error(f"Unexpected error during git diff: {e}", exc_info=logger.level <= logging.DEBUG); return f"Error during git diff: {e}"
82
59
 
83
- @function_tool
84
- def git_add(file_path: str = ".") -> str:
60
+ def _git_add_logic(file_path: str = ".") -> str:
85
61
  """Executes 'git add' to stage changes for the specified file or all changes (default '.')."""
86
- logger.info(f"Executing git add {file_path}") # Keep INFO for tool execution start
62
+ logger.info(f"Executing git add {file_path}")
87
63
  try:
88
64
  result = subprocess.run(["git", "add", file_path], capture_output=True, text=True, check=True, timeout=30)
89
65
  logger.debug(f"Git add '{file_path}' completed successfully.")
90
66
  return f"OK: Staged '{file_path}' successfully."
91
- except FileNotFoundError:
92
- logger.error("Git command not found.")
93
- return "Error: git command not found."
94
- except subprocess.CalledProcessError as e:
95
- logger.error(f"Error executing git add '{file_path}': {e.stderr}")
96
- return f"Error executing git add '{file_path}': {e.stderr}"
97
- except subprocess.TimeoutExpired:
98
- logger.error(f"Git add command timed out for '{file_path}'.")
99
- return f"Error: Git add command timed out for '{file_path}'."
100
- except Exception as e:
101
- logger.error(f"Unexpected error during git add '{file_path}': {e}", exc_info=logger.level <= logging.DEBUG)
102
- return f"Error during git add '{file_path}': {e}"
67
+ except FileNotFoundError: logger.error("Git command not found."); return "Error: git command not found."
68
+ except subprocess.CalledProcessError as e: logger.error(f"Error executing git add '{file_path}': {e.stderr}"); return f"Error executing git add '{file_path}': {e.stderr}"
69
+ except subprocess.TimeoutExpired: logger.error(f"Git add command timed out for '{file_path}'."); return f"Error: Git add command timed out for '{file_path}'."
70
+ except Exception as e: logger.error(f"Unexpected error during git add '{file_path}': {e}", exc_info=logger.level <= logging.DEBUG); return f"Error during git add '{file_path}': {e}"
103
71
 
104
- @function_tool
105
- def git_commit(message: str) -> str:
72
+ def _git_commit_logic(message: str) -> str:
106
73
  """Executes 'git commit' with a provided commit message."""
107
- logger.info(f"Executing git commit -m '{message[:50]}...'") # Keep INFO for tool execution start
108
- if not message or not message.strip():
109
- logger.warning("Git commit attempted with empty or whitespace-only message.")
110
- return "Error: Commit message cannot be empty."
74
+ logger.info(f"Executing git commit -m '{message[:50]}...'")
75
+ if not message or not message.strip(): logger.warning("Git commit attempted with empty message."); return "Error: Commit message cannot be empty."
111
76
  try:
112
- # Using list form is generally safer than shell=True for complex args
113
- result = subprocess.run(["git", "commit", "-m", message], capture_output=True, text=True, check=False, timeout=30) # Use check=False
114
- output = result.stdout.strip()
115
- stderr = result.stderr.strip()
77
+ result = subprocess.run(["git", "commit", "-m", message], capture_output=True, text=True, check=False, timeout=30)
78
+ output = result.stdout.strip(); stderr = result.stderr.strip()
116
79
  logger.debug(f"Git commit raw output (Exit Code {result.returncode}):\nSTDOUT: {output}\nSTDERR: {stderr}")
117
-
118
- # Handle common non-error cases explicitly
119
80
  if "nothing to commit" in output or "nothing added to commit" in output or "no changes added to commit" in output:
120
- logger.info("Git commit reported: Nothing to commit.")
121
- return "OK: Nothing to commit."
122
- if result.returncode == 0:
123
- return f"OK: Committed with message '{message}'.\n{output}"
124
- else:
125
- # Log specific error if available
126
- error_detail = stderr if stderr else output
127
- logger.error(f"Error executing git commit (Exit Code {result.returncode}): {error_detail}")
128
- return f"Error executing git commit: {error_detail}"
81
+ logger.info("Git commit reported: Nothing to commit."); return "OK: Nothing to commit."
82
+ if result.returncode == 0: return f"OK: Committed with message '{message}'.\n{output}"
83
+ else: error_detail = stderr if stderr else output; logger.error(f"Error executing git commit (Exit Code {result.returncode}): {error_detail}"); return f"Error executing git commit: {error_detail}"
84
+ except FileNotFoundError: logger.error("Git command not found."); return "Error: git command not found."
85
+ except subprocess.TimeoutExpired: logger.error("Git commit command timed out."); return "Error: Git commit command timed out."
86
+ except Exception as e: logger.error(f"Unexpected error during git commit: {e}", exc_info=logger.level <= logging.DEBUG); return f"Error during git commit: {e}"
129
87
 
130
- except FileNotFoundError:
131
- logger.error("Git command not found.")
132
- return "Error: git command not found."
133
- except subprocess.TimeoutExpired:
134
- logger.error("Git commit command timed out.")
135
- return "Error: Git commit command timed out."
136
- except Exception as e:
137
- logger.error(f"Unexpected error during git commit: {e}", exc_info=logger.level <= logging.DEBUG)
138
- return f"Error during git commit: {e}"
139
-
140
- @function_tool
141
- def git_push() -> str:
88
+ def _git_push_logic() -> str:
142
89
  """Executes 'git push' to push staged commits to the remote repository."""
143
- logger.info("Executing git push") # Keep INFO for tool execution start
90
+ logger.info("Executing git push")
144
91
  try:
145
- result = subprocess.run(["git", "push"], capture_output=True, text=True, check=True, timeout=120) # Longer timeout for push
146
- output = result.stdout.strip() + "\n" + result.stderr.strip() # Combine stdout/stderr
92
+ result = subprocess.run(["git", "push"], capture_output=True, text=True, check=True, timeout=120)
93
+ output = result.stdout.strip() + "\n" + result.stderr.strip()
147
94
  logger.debug(f"Git push raw output:\n{output}")
148
95
  return f"OK: Push completed.\n{output.strip()}"
149
- except FileNotFoundError:
150
- logger.error("Git command not found.")
151
- return "Error: git command not found."
152
- except subprocess.CalledProcessError as e:
153
- error_output = e.stdout.strip() + "\n" + e.stderr.strip()
154
- logger.error(f"Error executing git push: {error_output}")
155
- return f"Error executing git push: {error_output.strip()}"
156
- except subprocess.TimeoutExpired:
157
- logger.error("Git push command timed out.")
158
- return "Error: Git push command timed out."
159
- except Exception as e:
160
- logger.error(f"Unexpected error during git push: {e}", exc_info=logger.level <= logging.DEBUG)
161
- return f"Error during git push: {e}"
96
+ except FileNotFoundError: logger.error("Git command not found."); return "Error: git command not found."
97
+ except subprocess.CalledProcessError as e: error_output = e.stdout.strip() + "\n" + e.stderr.strip(); logger.error(f"Error executing git push: {error_output}"); return f"Error executing git push: {error_output.strip()}"
98
+ except subprocess.TimeoutExpired: logger.error("Git push command timed out."); return "Error: Git push command timed out."
99
+ except Exception as e: logger.error(f"Unexpected error during git push: {e}", exc_info=logger.level <= logging.DEBUG); return f"Error during git push: {e}"
162
100
 
163
- @function_tool
164
- def run_npm_test(args: str = "") -> str:
101
+ def _run_npm_test_logic(args: str = "") -> str:
165
102
  """Executes 'npm run test' with optional arguments."""
166
103
  try:
167
- # Use shlex.split for safer argument handling if args are provided
168
- cmd_list = ["npm", "run", "test"] + (shlex.split(args) if args else [])
169
- cmd_str = ' '.join(cmd_list) # For logging
170
- logger.info(f"Executing npm test: {cmd_str}") # Keep INFO for tool execution start
171
- result = subprocess.run(cmd_list, capture_output=True, text=True, check=False, timeout=120) # check=False to capture output on failure
104
+ cmd_list = ["npm", "run", "test"] + (shlex.split(args) if args else []); cmd_str = ' '.join(cmd_list)
105
+ logger.info(f"Executing npm test: {cmd_str}")
106
+ result = subprocess.run(cmd_list, capture_output=True, text=True, check=False, timeout=120)
172
107
  output = f"Exit Code: {result.returncode}\nSTDOUT:\n{result.stdout.strip()}\nSTDERR:\n{result.stderr.strip()}"
173
- if result.returncode == 0:
174
- logger.debug(f"npm test completed successfully:\n{output}")
175
- return f"OK: npm test finished.\n{output}"
176
- else:
177
- logger.error(f"npm test failed (Exit Code {result.returncode}):\n{output}")
178
- return f"Error: npm test failed.\n{output}"
179
- except FileNotFoundError:
180
- logger.error("npm command not found. Is Node.js/npm installed and in PATH?")
181
- return "Error: npm command not found."
182
- except subprocess.TimeoutExpired:
183
- logger.error("npm test command timed out.")
184
- return "Error: npm test command timed out."
185
- except Exception as e:
186
- logger.error(f"Unexpected error during npm test: {e}", exc_info=logger.level <= logging.DEBUG)
187
- return f"Error during npm test: {e}"
108
+ if result.returncode == 0: logger.debug(f"npm test completed successfully:\n{output}"); return f"OK: npm test finished.\n{output}"
109
+ else: logger.error(f"npm test failed (Exit Code {result.returncode}):\n{output}"); return f"Error: npm test failed.\n{output}"
110
+ except FileNotFoundError: logger.error("npm command not found."); return "Error: npm command not found."
111
+ except subprocess.TimeoutExpired: logger.error("npm test command timed out."); return "Error: npm test command timed out."
112
+ except Exception as e: logger.error(f"Unexpected error during npm test: {e}", exc_info=logger.level <= logging.DEBUG); return f"Error during npm test: {e}"
188
113
 
189
- @function_tool
190
- def run_pytest(args: str = "") -> str:
114
+ def _run_pytest_logic(args: str = "") -> str:
191
115
  """Executes 'uv run pytest' with optional arguments."""
192
116
  try:
193
- # Use shlex.split for safer argument handling
194
- cmd_list = ["uv", "run", "pytest"] + (shlex.split(args) if args else [])
195
- cmd_str = ' '.join(cmd_list) # For logging
196
- logger.info(f"Executing pytest via uv: {cmd_str}") # Keep INFO for tool execution start
197
- result = subprocess.run(cmd_list, capture_output=True, text=True, check=False, timeout=120) # check=False to capture output on failure
117
+ cmd_list = ["uv", "run", "pytest"] + (shlex.split(args) if args else []); cmd_str = ' '.join(cmd_list)
118
+ logger.info(f"Executing pytest via uv: {cmd_str}")
119
+ result = subprocess.run(cmd_list, capture_output=True, text=True, check=False, timeout=120)
198
120
  output = f"Exit Code: {result.returncode}\nSTDOUT:\n{result.stdout.strip()}\nSTDERR:\n{result.stderr.strip()}"
199
- # Pytest often returns non-zero exit code on test failures, report this clearly
200
- if result.returncode == 0:
201
- logger.debug(f"pytest completed successfully:\n{output}")
202
- return f"OK: pytest finished successfully.\n{output}"
203
- else:
204
- logger.warning(f"pytest finished with failures (Exit Code {result.returncode}):\n{output}")
205
- # Still return "OK" from tool perspective, but indicate failure in the message
206
- return f"OK: Pytest finished with failures (Exit Code {result.returncode}).\n{output}"
207
- except FileNotFoundError:
208
- logger.error("uv command not found. Is uv installed and in PATH?")
209
- return "Error: uv command not found."
210
- except subprocess.TimeoutExpired:
211
- logger.error("pytest command timed out.")
212
- return "Error: pytest command timed out."
213
- except Exception as e:
214
- logger.error(f"Unexpected error during pytest: {e}", exc_info=logger.level <= logging.DEBUG)
215
- return f"Error during pytest: {e}"
121
+ if result.returncode == 0: logger.debug(f"pytest completed successfully:\n{output}"); return f"OK: pytest finished successfully.\n{output}"
122
+ else: logger.warning(f"pytest finished with failures (Exit Code {result.returncode}):\n{output}"); return f"OK: Pytest finished with failures (Exit Code {result.returncode}).\n{output}"
123
+ except FileNotFoundError: logger.error("uv command not found."); return "Error: uv command not found."
124
+ except subprocess.TimeoutExpired: logger.error("pytest command timed out."); return "Error: pytest command timed out."
125
+ except Exception as e: logger.error(f"Unexpected error during pytest: {e}", exc_info=logger.level <= logging.DEBUG); return f"Error during pytest: {e}"
126
+
127
+ # --- Tool Definitions (Decorated - reverted to default naming) ---
128
+ git_status = function_tool(_git_status_logic)
129
+ git_diff = function_tool(_git_diff_logic)
130
+ git_add = function_tool(_git_add_logic)
131
+ git_commit = function_tool(_git_commit_logic)
132
+ git_push = function_tool(_git_push_logic)
133
+ run_npm_test = function_tool(_run_npm_test_logic)
134
+ run_pytest = function_tool(_run_pytest_logic)
216
135
 
217
136
  # --- Agent Instructions ---
218
- # Define clear instructions for each agent's role and capabilities.
137
+ # (Instructions remain the same)
219
138
  michael_instructions = """
220
139
  You are Michael Toasted, the resolute leader of the Burnt Noodles creative team.
221
140
  Your primary role is to understand the user's request, break it down into actionable steps,
@@ -245,50 +164,28 @@ Available Agent Tools: None (Report back to Michael for delegation).
245
164
  """
246
165
 
247
166
  # --- Blueprint Definition ---
248
- # Inherits from BlueprintBase, defines metadata, creates agents, and sets up delegation.
249
167
  class BurntNoodlesBlueprint(BlueprintBase):
250
- """
251
- Burnt Noodles Blueprint: A multi-agent team demonstrating Git operations and testing workflows.
252
- - Michael Toasted: Coordinator, delegates tasks.
253
- - Fiona Flame: Handles Git commands (status, diff, add, commit, push).
254
- - Sam Ashes: Handles test execution (npm, pytest).
255
- """
256
- # Class variable for blueprint metadata, conforming to BlueprintBase structure.
257
168
  metadata: ClassVar[Dict[str, Any]] = {
258
169
  "name": "BurntNoodlesBlueprint",
259
170
  "title": "Burnt Noodles",
260
171
  "description": "A multi-agent team managing Git operations and code testing.",
261
- "version": "1.1.0", # Incremented version
172
+ "version": "1.1.0",
262
173
  "author": "Open Swarm Team (Refactored)",
263
174
  "tags": ["git", "test", "multi-agent", "collaboration", "refactor"],
264
- "required_mcp_servers": [], # No external MCP servers needed for core functionality
175
+ "required_mcp_servers": [],
265
176
  }
266
177
 
267
- # Caches for OpenAI client and Model instances to avoid redundant creation.
268
178
  _openai_client_cache: Dict[str, AsyncOpenAI] = {}
269
179
  _model_instance_cache: Dict[str, Model] = {}
270
180
 
271
181
  def _get_model_instance(self, profile_name: str) -> Model:
272
- """
273
- Retrieves or creates an LLM Model instance based on the configuration profile.
274
- Handles client instantiation and caching. Uses OpenAIChatCompletionsModel.
275
- Args:
276
- profile_name: The name of the LLM profile to use (e.g., 'default').
277
- Returns:
278
- An instance of the configured Model.
279
- Raises:
280
- ValueError: If configuration is missing or invalid.
281
- """
282
- # Check cache first
283
182
  if profile_name in self._model_instance_cache:
284
183
  logger.debug(f"Using cached Model instance for profile '{profile_name}'.")
285
184
  return self._model_instance_cache[profile_name]
286
185
 
287
186
  logger.debug(f"Creating new Model instance for profile '{profile_name}'.")
288
- # Retrieve profile data using BlueprintBase helper method
289
- profile_data = self.get_llm_profile(profile_name)
187
+ profile_data = getattr(self, "get_llm_profile", lambda prof: {"provider": "openai", "model": "gpt-mock"})(profile_name)
290
188
  if not profile_data:
291
- # Critical error if the profile (or default fallback) isn't found
292
189
  logger.critical(f"Cannot create Model instance: LLM profile '{profile_name}' (or 'default') not found in configuration.")
293
190
  raise ValueError(f"Missing LLM profile configuration for '{profile_name}' or 'default'.")
294
191
 
@@ -298,21 +195,17 @@ class BurntNoodlesBlueprint(BlueprintBase):
298
195
  logger.critical(f"LLM profile '{profile_name}' is missing the required 'model' key.")
299
196
  raise ValueError(f"Missing 'model' key in LLM profile '{profile_name}'.")
300
197
 
301
- # Ensure we only handle OpenAI for now
302
198
  if provider != "openai":
303
199
  logger.error(f"Unsupported LLM provider '{provider}' in profile '{profile_name}'. Only 'openai' is supported in this blueprint.")
304
200
  raise ValueError(f"Unsupported LLM provider: {provider}")
305
201
 
306
- # Create or retrieve cached OpenAI client instance
307
202
  client_cache_key = f"{provider}_{profile_data.get('base_url')}"
308
203
  if client_cache_key not in self._openai_client_cache:
309
- # Prepare arguments for AsyncOpenAI, filtering out None values
310
204
  client_kwargs = { "api_key": profile_data.get("api_key"), "base_url": profile_data.get("base_url") }
311
205
  filtered_client_kwargs = {k: v for k, v in client_kwargs.items() if v is not None}
312
- log_client_kwargs = {k:v for k,v in filtered_client_kwargs.items() if k != 'api_key'} # Don't log API key
206
+ log_client_kwargs = {k:v for k,v in filtered_client_kwargs.items() if k != 'api_key'}
313
207
  logger.debug(f"Creating new AsyncOpenAI client for profile '{profile_name}' with config: {log_client_kwargs}")
314
208
  try:
315
- # Create and cache the client
316
209
  self._openai_client_cache[client_cache_key] = AsyncOpenAI(**filtered_client_kwargs)
317
210
  except Exception as e:
318
211
  logger.error(f"Failed to create AsyncOpenAI client for profile '{profile_name}': {e}", exc_info=True)
@@ -320,11 +213,9 @@ class BurntNoodlesBlueprint(BlueprintBase):
320
213
 
321
214
  openai_client_instance = self._openai_client_cache[client_cache_key]
322
215
 
323
- # Instantiate the specific Model implementation (OpenAIChatCompletionsModel)
324
216
  logger.debug(f"Instantiating OpenAIChatCompletionsModel(model='{model_name}') with client instance for profile '{profile_name}'.")
325
217
  try:
326
218
  model_instance = OpenAIChatCompletionsModel(model=model_name, openai_client=openai_client_instance)
327
- # Cache the model instance
328
219
  self._model_instance_cache[profile_name] = model_instance
329
220
  return model_instance
330
221
  except Exception as e:
@@ -332,81 +223,82 @@ class BurntNoodlesBlueprint(BlueprintBase):
332
223
  raise ValueError(f"Failed to initialize LLM provider for profile '{profile_name}': {e}") from e
333
224
 
334
225
  def create_starting_agent(self, mcp_servers: List[MCPServer]) -> Agent:
335
- """
336
- Creates the Burnt Noodles agent team: Michael (Coordinator), Fiona (Git), Sam (Testing).
337
- Sets up tools and agent-as-tool delegation.
338
- Args:
339
- mcp_servers: List of started MCP server instances (not used by this BP).
340
- Returns:
341
- The starting agent instance (Michael Toasted).
342
- """
343
226
  logger.debug("Creating Burnt Noodles agent team...")
344
- # Clear caches at the start of agent creation for this run
227
+ config = self._load_configuration() if getattr(self, "config", None) is None else self.config
345
228
  self._model_instance_cache = {}
346
229
  self._openai_client_cache = {}
347
230
 
348
- # Determine the LLM profile to use (e.g., from config or default)
349
- default_profile_name = self.config.get("llm_profile", "default")
231
+ default_profile_name = config.get("llm_profile", "default")
350
232
  logger.debug(f"Using LLM profile '{default_profile_name}' for all Burnt Noodles agents.")
351
- # Get the single Model instance to share among agents (or create if needed)
352
233
  default_model_instance = self._get_model_instance(default_profile_name)
353
234
 
354
- # Instantiate the specialist agents first
355
- # Fiona gets Git function tools
235
+ # --- Use the decorated tool variables ---
356
236
  fiona_flame = Agent(
357
- name="Fiona_Flame", # Use names valid as tool names
237
+ name="Fiona_Flame",
358
238
  model=default_model_instance,
359
239
  instructions=fiona_instructions,
360
240
  tools=[git_status, git_diff, git_add, git_commit, git_push] # Agent tools added later
361
241
  )
362
- # Sam gets Testing function tools
363
242
  sam_ashes = Agent(
364
- name="Sam_Ashes", # Use names valid as tool names
243
+ name="Sam_Ashes",
365
244
  model=default_model_instance,
366
245
  instructions=sam_instructions,
367
246
  tools=[run_npm_test, run_pytest] # Agent tools added later
368
247
  )
369
-
370
- # Instantiate the coordinator agent (Michael)
371
- # Michael gets limited function tools and the specialist agents as tools
372
248
  michael_toasted = Agent(
373
249
  name="Michael_Toasted",
374
250
  model=default_model_instance,
375
251
  instructions=michael_instructions,
376
252
  tools=[
377
- # Michael's direct function tools (limited scope)
378
- git_status,
253
+ git_status, # Michael's direct tools
379
254
  git_diff,
380
- # Specialist agents exposed as tools for delegation
381
255
  fiona_flame.as_tool(
382
- tool_name="Fiona_Flame", # Explicit tool name
256
+ tool_name="Fiona_Flame",
383
257
  tool_description="Delegate Git operations (add, commit, push) or complex status/diff queries to Fiona."
384
258
  ),
385
259
  sam_ashes.as_tool(
386
- tool_name="Sam_Ashes", # Explicit tool name
260
+ tool_name="Sam_Ashes",
387
261
  tool_description="Delegate testing tasks (npm test, pytest) to Sam."
388
262
  ),
389
263
  ],
390
- mcp_servers=mcp_servers # Pass along MCP servers if needed (though not used here)
264
+ mcp_servers=mcp_servers
391
265
  )
266
+ # --- End tool variable usage ---
392
267
 
393
- # Add cross-delegation tools *after* all agents are instantiated
394
- # Fiona can delegate testing to Sam
395
268
  fiona_flame.tools.append(
396
269
  sam_ashes.as_tool(tool_name="Sam_Ashes", tool_description="Delegate testing tasks (npm test, pytest) to Sam.")
397
270
  )
398
- # Sam can delegate Git tasks back to Fiona (as per instructions, Sam should report to Michael,
399
- # but having the tool technically available might be useful in complex future scenarios,
400
- # rely on prompt engineering to prevent direct calls unless intended).
401
- # sam_ashes.tools.append(
402
- # fiona_flame.as_tool(tool_name="Fiona_Flame", tool_description="Delegate Git operations back to Fiona if needed.")
403
- # )
404
271
 
405
272
  logger.debug("Burnt Noodles agent team created successfully. Michael Toasted is the starting agent.")
406
- # Return the coordinator agent as the entry point for the Runner
407
273
  return michael_toasted
408
274
 
275
+ async def run(self, messages: List[Dict[str, Any]], **kwargs) -> AsyncGenerator[Dict[str, Any], None]:
276
+ """
277
+ Main execution entry point for the Burnt Noodles blueprint.
278
+ Delegates to _run_non_interactive for CLI-like execution.
279
+ """
280
+ logger.info("BurntNoodlesBlueprint run method called.")
281
+ instruction = messages[-1].get("content", "") if messages else ""
282
+ async for chunk in self._run_non_interactive(instruction, **kwargs):
283
+ yield chunk
284
+ logger.info("BurntNoodlesBlueprint run method finished.")
285
+
286
+ async def _run_non_interactive(self, instruction: str, **kwargs) -> AsyncGenerator[Dict[str, Any], None]:
287
+ """Helper to run the agent flow based on an instruction."""
288
+ logger.info(f"Running Burnt Noodles non-interactively with instruction: '{instruction[:100]}...'")
289
+ mcp_servers = kwargs.get("mcp_servers", [])
290
+ starting_agent = self.create_starting_agent(mcp_servers=mcp_servers)
291
+ runner = Runner(agent=starting_agent)
292
+ try:
293
+ final_result = await runner.run(instruction)
294
+ logger.info(f"Non-interactive run finished. Final Output: {final_result.final_output}")
295
+ yield { "messages": [ {"role": "assistant", "content": final_result.final_output} ] }
296
+ except Exception as e:
297
+ logger.error(f"Error during non-interactive run: {e}", exc_info=True)
298
+ yield { "messages": [ {"role": "assistant", "content": f"An error occurred: {e}"} ] }
299
+
300
+
409
301
  # Standard Python entry point for direct script execution
410
302
  if __name__ == "__main__":
411
- # Call the main class method from BlueprintBase to handle CLI parsing and execution.
412
303
  BurntNoodlesBlueprint.main()
304
+
@@ -0,0 +1,42 @@
1
+ import abc
2
+ from typing import List, Dict, Any, AsyncGenerator
3
+
4
+ # Assuming blueprint_base is in the same directory or accessible via installed package
5
+ from .blueprint_base import BlueprintBase
6
+
7
+ class RunnableBlueprint(BlueprintBase, abc.ABC):
8
+ """
9
+ Abstract base class for blueprints designed to be executed programmatically,
10
+ typically via an API endpoint like swarm-api.
11
+
12
+ Inherits common functionality from BlueprintBase and requires subclasses
13
+ to implement the `run` method as the standard entry point for execution.
14
+ """
15
+
16
+ @abc.abstractmethod
17
+ async def run(self, messages: List[Dict[str, Any]], **kwargs) -> AsyncGenerator[Dict[str, Any], None]:
18
+ """
19
+ Abstract method defining the standard entry point for running the blueprint
20
+ programmatically.
21
+
22
+ Args:
23
+ messages: A list of message dictionaries, typically following the
24
+ OpenAI chat completions format. The last message is usually
25
+ the user's input or instruction.
26
+ **kwargs: Additional keyword arguments that might be passed by the
27
+ runner (e.g., mcp_servers, configuration overrides).
28
+
29
+ Yields:
30
+ Dictionaries representing chunks of the response, often containing
31
+ a 'messages' key with a list of message objects. The exact format
32
+ may depend on the runner's expectations (e.g., SSE for streaming).
33
+
34
+ Raises:
35
+ NotImplementedError: If the subclass does not implement this method.
36
+ """
37
+ raise NotImplementedError("Subclasses of RunnableBlueprint must implement the 'run' method.")
38
+ # This yield is technically unreachable but satisfies static analysis
39
+ # expecting a generator function body.
40
+ if False:
41
+ yield {}
42
+
swarm/middleware.py ADDED
@@ -0,0 +1,65 @@
1
+ # src/swarm/middleware.py
2
+ import logging
3
+ import asyncio # Import asyncio
4
+ from asgiref.sync import sync_to_async
5
+ from django.utils.functional import SimpleLazyObject
6
+ from django.utils.decorators import sync_and_async_middleware
7
+ from django.contrib.auth.middleware import AuthenticationMiddleware
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ # Mark the middleware as compatible with both sync and async views
12
+ @sync_and_async_middleware
13
+ def AsyncAuthMiddleware(get_response):
14
+ """
15
+ Ensures request.user is loaded asynchronously before reaching async views,
16
+ preventing SynchronousOnlyOperation errors during authentication checks
17
+ that might involve database access (like session loading).
18
+
19
+ This should be placed *after* Django's built-in AuthenticationMiddleware.
20
+ """
21
+
22
+ # One-time configuration and initialization.
23
+ # (Not needed for this simple middleware)
24
+
25
+ async def middleware(request):
26
+ # Code to be executed for each request before
27
+ # the view (and later middleware) are called.
28
+
29
+ # Check if request.user is a SimpleLazyObject and hasn't been evaluated yet.
30
+ # Django's AuthenticationMiddleware sets request.user to a SimpleLazyObject
31
+ # wrapping the get_user function. Accessing request.user triggers evaluation.
32
+ if isinstance(request.user, SimpleLazyObject):
33
+ # Use sync_to_async to safely evaluate the lazy object (which calls
34
+ # the synchronous get_user function) in an async context.
35
+ # We don't need the result here, just to trigger the load.
36
+ try:
37
+ logger.debug("[AsyncAuthMiddleware] Attempting async user load...")
38
+ _ = await sync_to_async(request.user._setup)() # Access internal _setup to force load
39
+ is_auth = await sync_to_async(lambda: getattr(request.user, 'is_authenticated', False))()
40
+ logger.debug(f"[AsyncAuthMiddleware] User loaded via SimpleLazyObject: {request.user}, Authenticated: {is_auth}")
41
+ except Exception as e:
42
+ # Log potential errors during user loading but don't block the request
43
+ logger.error(f"[AsyncAuthMiddleware] Error during async user load: {e}", exc_info=True)
44
+ # You might want to handle specific auth errors differently
45
+ else:
46
+ # If it's not a SimpleLazyObject, it might be already loaded or AnonymousUser
47
+ is_auth = getattr(request.user, 'is_authenticated', False)
48
+ logger.debug(f"[AsyncAuthMiddleware] User already loaded or not lazy: {request.user}, Authenticated: {is_auth}")
49
+
50
+
51
+ response = await get_response(request)
52
+
53
+ # Code to be executed for each request/response after
54
+ # the view is called.
55
+
56
+ return response
57
+
58
+ # Return the correct function based on whether get_response is async or sync
59
+ if asyncio.iscoroutinefunction(get_response):
60
+ return middleware
61
+ else:
62
+ # If the next middleware/view is sync, we don't need our async wrapper
63
+ # However, the decorator handles this, so we just return the async version.
64
+ # For clarity, the decorator makes this middleware compatible either way.
65
+ return middleware
swarm/settings.py CHANGED
@@ -28,6 +28,8 @@ ENABLE_API_AUTH = bool(_raw_api_token)
28
28
  SWARM_API_KEY = _raw_api_token # Assign the loaded token (or None)
29
29
 
30
30
  if ENABLE_API_AUTH:
31
+ # Add assertion to satisfy type checkers within this block
32
+ assert SWARM_API_KEY is not None, "SWARM_API_KEY cannot be None when ENABLE_API_AUTH is True"
31
33
  print(f"[Settings] SWARM_API_KEY loaded: {SWARM_API_KEY[:4]}...{SWARM_API_KEY[-4:]}")
32
34
  print("[Settings] ENABLE_API_AUTH is True.")
33
35
  else:
@@ -58,6 +60,8 @@ MIDDLEWARE = [
58
60
  'django.middleware.common.CommonMiddleware',
59
61
  'django.middleware.csrf.CsrfViewMiddleware',
60
62
  'django.contrib.auth.middleware.AuthenticationMiddleware',
63
+ # Add custom middleware to handle async user loading after standard auth
64
+ 'swarm.middleware.AsyncAuthMiddleware',
61
65
  'django.contrib.messages.middleware.MessageMiddleware',
62
66
  'django.middleware.clickjacking.XFrameOptionsMiddleware',
63
67
  ]