open-swarm 0.1.1743070217__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.1743070217.dist-info/METADATA +258 -0
- open_swarm-0.1.1743070217.dist-info/RECORD +89 -0
- open_swarm-0.1.1743070217.dist-info/WHEEL +5 -0
- open_swarm-0.1.1743070217.dist-info/entry_points.txt +3 -0
- open_swarm-0.1.1743070217.dist-info/licenses/LICENSE +21 -0
- open_swarm-0.1.1743070217.dist-info/top_level.txt +1 -0
- swarm/__init__.py +3 -0
- swarm/agent/__init__.py +7 -0
- swarm/agent/agent.py +49 -0
- swarm/apps.py +53 -0
- swarm/auth.py +56 -0
- swarm/consumers.py +141 -0
- swarm/core.py +326 -0
- swarm/extensions/__init__.py +1 -0
- swarm/extensions/blueprint/__init__.py +36 -0
- swarm/extensions/blueprint/agent_utils.py +45 -0
- swarm/extensions/blueprint/blueprint_base.py +562 -0
- swarm/extensions/blueprint/blueprint_discovery.py +112 -0
- swarm/extensions/blueprint/blueprint_utils.py +17 -0
- swarm/extensions/blueprint/common_utils.py +12 -0
- swarm/extensions/blueprint/django_utils.py +203 -0
- swarm/extensions/blueprint/interactive_mode.py +102 -0
- swarm/extensions/blueprint/modes/rest_mode.py +37 -0
- swarm/extensions/blueprint/output_utils.py +95 -0
- swarm/extensions/blueprint/spinner.py +91 -0
- swarm/extensions/cli/__init__.py +0 -0
- swarm/extensions/cli/blueprint_runner.py +251 -0
- swarm/extensions/cli/cli_args.py +88 -0
- swarm/extensions/cli/commands/__init__.py +0 -0
- swarm/extensions/cli/commands/blueprint_management.py +31 -0
- swarm/extensions/cli/commands/config_management.py +15 -0
- swarm/extensions/cli/commands/edit_config.py +77 -0
- swarm/extensions/cli/commands/list_blueprints.py +22 -0
- swarm/extensions/cli/commands/validate_env.py +57 -0
- swarm/extensions/cli/commands/validate_envvars.py +39 -0
- swarm/extensions/cli/interactive_shell.py +41 -0
- swarm/extensions/cli/main.py +36 -0
- swarm/extensions/cli/selection.py +43 -0
- swarm/extensions/cli/utils/discover_commands.py +32 -0
- swarm/extensions/cli/utils/env_setup.py +15 -0
- swarm/extensions/cli/utils.py +105 -0
- swarm/extensions/config/__init__.py +6 -0
- swarm/extensions/config/config_loader.py +208 -0
- swarm/extensions/config/config_manager.py +258 -0
- swarm/extensions/config/server_config.py +49 -0
- swarm/extensions/config/setup_wizard.py +103 -0
- swarm/extensions/config/utils/__init__.py +0 -0
- swarm/extensions/config/utils/logger.py +36 -0
- swarm/extensions/launchers/__init__.py +1 -0
- swarm/extensions/launchers/build_launchers.py +14 -0
- swarm/extensions/launchers/build_swarm_wrapper.py +12 -0
- swarm/extensions/launchers/swarm_api.py +68 -0
- swarm/extensions/launchers/swarm_cli.py +304 -0
- swarm/extensions/launchers/swarm_wrapper.py +29 -0
- swarm/extensions/mcp/__init__.py +1 -0
- swarm/extensions/mcp/cache_utils.py +36 -0
- swarm/extensions/mcp/mcp_client.py +341 -0
- swarm/extensions/mcp/mcp_constants.py +7 -0
- swarm/extensions/mcp/mcp_tool_provider.py +110 -0
- swarm/llm/chat_completion.py +195 -0
- swarm/messages.py +132 -0
- swarm/migrations/0010_initial_chat_models.py +51 -0
- swarm/migrations/__init__.py +0 -0
- swarm/models.py +45 -0
- swarm/repl/__init__.py +1 -0
- swarm/repl/repl.py +87 -0
- swarm/serializers.py +12 -0
- swarm/settings.py +189 -0
- swarm/tool_executor.py +239 -0
- swarm/types.py +126 -0
- swarm/urls.py +89 -0
- swarm/util.py +124 -0
- swarm/utils/color_utils.py +40 -0
- swarm/utils/context_utils.py +272 -0
- swarm/utils/general_utils.py +162 -0
- swarm/utils/logger.py +61 -0
- swarm/utils/logger_setup.py +25 -0
- swarm/utils/message_sequence.py +173 -0
- swarm/utils/message_utils.py +95 -0
- swarm/utils/redact.py +68 -0
- swarm/views/__init__.py +41 -0
- swarm/views/api_views.py +46 -0
- swarm/views/chat_views.py +76 -0
- swarm/views/core_views.py +118 -0
- swarm/views/message_views.py +40 -0
- swarm/views/model_views.py +135 -0
- swarm/views/utils.py +457 -0
- swarm/views/web_views.py +149 -0
- swarm/wsgi.py +16 -0
@@ -0,0 +1,562 @@
|
|
1
|
+
"""
|
2
|
+
Swarm Blueprint Base Module (Sync Interactive Mode)
|
3
|
+
"""
|
4
|
+
|
5
|
+
import asyncio
|
6
|
+
import json
|
7
|
+
import logging
|
8
|
+
from src.swarm.utils.message_sequence import repair_message_payload, validate_message_sequence
|
9
|
+
from src.swarm.utils.context_utils import truncate_message_history, get_token_count
|
10
|
+
import os
|
11
|
+
import uuid
|
12
|
+
import sys
|
13
|
+
from abc import ABC, abstractmethod
|
14
|
+
from typing import Optional, Dict, Any, List
|
15
|
+
|
16
|
+
from pathlib import Path
|
17
|
+
from swarm.core import Swarm
|
18
|
+
from swarm.extensions.config.config_loader import load_server_config
|
19
|
+
from swarm.settings import DEBUG
|
20
|
+
from swarm.utils.redact import redact_sensitive_data
|
21
|
+
from swarm.utils.context_utils import get_token_count, truncate_message_history
|
22
|
+
from swarm.extensions.blueprint.agent_utils import (
|
23
|
+
get_agent_name,
|
24
|
+
discover_tools_for_agent,
|
25
|
+
discover_resources_for_agent,
|
26
|
+
initialize_agents
|
27
|
+
)
|
28
|
+
from swarm.extensions.blueprint.django_utils import register_django_components
|
29
|
+
from swarm.extensions.blueprint.spinner import Spinner
|
30
|
+
from swarm.extensions.blueprint.output_utils import pretty_print_response
|
31
|
+
from dotenv import load_dotenv
|
32
|
+
import argparse
|
33
|
+
from swarm.types import Agent, Response
|
34
|
+
|
35
|
+
logger = logging.getLogger(__name__)
|
36
|
+
|
37
|
+
class BlueprintBase(ABC):
|
38
|
+
"""Base class for Swarm blueprints with sync interactive mode and Django integration."""
|
39
|
+
|
40
|
+
def __init__(
|
41
|
+
self,
|
42
|
+
config: dict,
|
43
|
+
auto_complete_task: bool = False,
|
44
|
+
update_user_goal: bool = False,
|
45
|
+
update_user_goal_frequency: int = 5,
|
46
|
+
skip_django_registration: bool = False,
|
47
|
+
record_chat: bool = False,
|
48
|
+
log_file_path: Optional[str] = None,
|
49
|
+
debug: bool = False,
|
50
|
+
use_markdown: bool = False,
|
51
|
+
**kwargs
|
52
|
+
):
|
53
|
+
self.auto_complete_task = auto_complete_task
|
54
|
+
self.update_user_goal = update_user_goal
|
55
|
+
self.update_user_goal_frequency = max(1, update_user_goal_frequency)
|
56
|
+
self.last_goal_update_count = 0
|
57
|
+
self.record_chat = record_chat
|
58
|
+
self.conversation_id = str(uuid.uuid4()) if record_chat else None
|
59
|
+
self.log_file_path = log_file_path
|
60
|
+
self.debug = debug or DEBUG
|
61
|
+
self.use_markdown = use_markdown
|
62
|
+
self._urls_registered = False
|
63
|
+
|
64
|
+
if self.use_markdown:
|
65
|
+
logger.debug("Markdown rendering enabled (if rich is available).")
|
66
|
+
logger.debug(f"Initializing {self.__class__.__name__} with config: {redact_sensitive_data(config)}")
|
67
|
+
if not hasattr(self, 'metadata') or not isinstance(self.metadata, dict):
|
68
|
+
raise AssertionError(f"{self.__class__.__name__} must define a 'metadata' property returning a dictionary.")
|
69
|
+
|
70
|
+
self.truncation_mode = os.getenv("SWARM_TRUNCATION_MODE", "pairs").lower()
|
71
|
+
self.max_context_tokens = max(1, self.metadata.get("max_context_tokens", 8000))
|
72
|
+
self.max_context_messages = max(1, self.metadata.get("max_context_messages", 50))
|
73
|
+
logger.debug(f"Truncation settings: mode={self.truncation_mode}, max_tokens={self.max_context_tokens}, max_messages={self.max_context_messages}")
|
74
|
+
|
75
|
+
load_dotenv()
|
76
|
+
logger.debug("Loaded environment variables from .env.")
|
77
|
+
|
78
|
+
self.config = config
|
79
|
+
self.skip_django_registration = skip_django_registration or not os.environ.get("DJANGO_SETTINGS_MODULE")
|
80
|
+
self.swarm = kwargs.get('swarm_instance') or Swarm(config=self.config, debug=self.debug)
|
81
|
+
logger.debug("Swarm instance initialized.")
|
82
|
+
|
83
|
+
self.context_variables: Dict[str, Any] = {"user_goal": ""}
|
84
|
+
self.starting_agent = None
|
85
|
+
self._discovered_tools: Dict[str, List[Any]] = {}
|
86
|
+
self._discovered_resources: Dict[str, List[Any]] = {}
|
87
|
+
self.spinner = Spinner(interactive=not kwargs.get('non_interactive', False))
|
88
|
+
|
89
|
+
required_env_vars = set(self.metadata.get('env_vars', []))
|
90
|
+
missing_vars = [var for var in required_env_vars if not os.getenv(var)]
|
91
|
+
if missing_vars:
|
92
|
+
logger.warning(f"Missing environment variables for {self.metadata.get('title', self.__class__.__name__)}: {', '.join(missing_vars)}")
|
93
|
+
|
94
|
+
self.required_mcp_servers = self.metadata.get('required_mcp_servers', [])
|
95
|
+
logger.debug(f"Required MCP servers: {self.required_mcp_servers}")
|
96
|
+
|
97
|
+
if self._is_create_agents_overridden():
|
98
|
+
initialize_agents(self)
|
99
|
+
register_django_components(self)
|
100
|
+
|
101
|
+
def _is_create_agents_overridden(self) -> bool:
|
102
|
+
"""Check if the 'create_agents' method is overridden in the subclass."""
|
103
|
+
return self.__class__.create_agents is not BlueprintBase.create_agents
|
104
|
+
|
105
|
+
def truncate_message_history(self, messages: List[Dict[str, Any]], model: str) -> List[Dict[str, Any]]:
|
106
|
+
"""Truncate message history using the centralized utility."""
|
107
|
+
return truncate_message_history(messages, model, self.max_context_tokens, self.max_context_messages)
|
108
|
+
|
109
|
+
@property
|
110
|
+
@abstractmethod
|
111
|
+
def metadata(self) -> Dict[str, Any]:
|
112
|
+
"""Abstract property for blueprint metadata."""
|
113
|
+
raise NotImplementedError
|
114
|
+
|
115
|
+
def create_agents(self) -> Dict[str, Agent]:
|
116
|
+
"""Default agent creation method."""
|
117
|
+
return {}
|
118
|
+
|
119
|
+
def set_starting_agent(self, agent: Agent) -> None:
|
120
|
+
"""Set the starting agent and trigger initial asset discovery."""
|
121
|
+
agent_name = get_agent_name(agent)
|
122
|
+
logger.debug(f"Setting starting agent to: {agent_name}")
|
123
|
+
self.starting_agent = agent
|
124
|
+
self.context_variables["active_agent_name"] = agent_name
|
125
|
+
|
126
|
+
try:
|
127
|
+
loop = asyncio.get_event_loop()
|
128
|
+
is_running = loop.is_running()
|
129
|
+
except RuntimeError:
|
130
|
+
loop = None
|
131
|
+
is_running = False
|
132
|
+
|
133
|
+
# Corrected calls: Pass only agent and config
|
134
|
+
if loop and is_running:
|
135
|
+
logger.debug(f"Scheduling async asset discovery for starting agent {agent_name}.")
|
136
|
+
asyncio.ensure_future(discover_tools_for_agent(agent, self.swarm.config))
|
137
|
+
asyncio.ensure_future(discover_resources_for_agent(agent, self.swarm.config))
|
138
|
+
else:
|
139
|
+
logger.debug(f"Running sync asset discovery for starting agent {agent_name}.")
|
140
|
+
try:
|
141
|
+
asyncio.run(discover_tools_for_agent(agent, self.swarm.config))
|
142
|
+
asyncio.run(discover_resources_for_agent(agent, self.swarm.config))
|
143
|
+
except RuntimeError as e:
|
144
|
+
if "cannot be called from a running event loop" in str(e): logger.error("Nested asyncio.run detected during sync discovery.")
|
145
|
+
else: raise e
|
146
|
+
|
147
|
+
async def determine_active_agent(self) -> Optional[Agent]:
|
148
|
+
"""Determine the currently active agent."""
|
149
|
+
active_agent_name = self.context_variables.get("active_agent_name")
|
150
|
+
agent_to_use = None
|
151
|
+
|
152
|
+
if active_agent_name and active_agent_name in self.swarm.agents:
|
153
|
+
agent_to_use = self.swarm.agents[active_agent_name]
|
154
|
+
logger.debug(f"Determined active agent from context: {active_agent_name}")
|
155
|
+
elif self.starting_agent:
|
156
|
+
agent_to_use = self.starting_agent
|
157
|
+
active_agent_name = get_agent_name(agent_to_use)
|
158
|
+
if self.context_variables.get("active_agent_name") != active_agent_name:
|
159
|
+
self.context_variables["active_agent_name"] = active_agent_name
|
160
|
+
logger.debug(f"Falling back to starting agent: {active_agent_name} and updating context.")
|
161
|
+
else:
|
162
|
+
logger.debug(f"Using starting agent: {active_agent_name}")
|
163
|
+
else:
|
164
|
+
logger.error("Cannot determine active agent: No agent name in context and no starting agent set.")
|
165
|
+
return None
|
166
|
+
|
167
|
+
agent_name_cache_key = get_agent_name(agent_to_use)
|
168
|
+
# Corrected calls: Pass only agent and config
|
169
|
+
if agent_name_cache_key not in self._discovered_tools:
|
170
|
+
logger.debug(f"Cache miss for tools of agent {agent_name_cache_key}. Discovering...")
|
171
|
+
discovered_tools = await discover_tools_for_agent(agent_to_use, self.swarm.config)
|
172
|
+
self._discovered_tools[agent_name_cache_key] = discovered_tools
|
173
|
+
|
174
|
+
if agent_name_cache_key not in self._discovered_resources:
|
175
|
+
logger.debug(f"Cache miss for resources of agent {agent_name_cache_key}. Discovering...")
|
176
|
+
discovered_resources = await discover_resources_for_agent(agent_to_use, self.swarm.config)
|
177
|
+
self._discovered_resources[agent_name_cache_key] = discovered_resources
|
178
|
+
|
179
|
+
return agent_to_use
|
180
|
+
|
181
|
+
# --- Core Execution Logic ---
|
182
|
+
def run_with_context(self, messages: List[Dict[str, str]], context_variables: dict) -> dict:
|
183
|
+
"""Synchronous wrapper for the async execution logic."""
|
184
|
+
return asyncio.run(self.run_with_context_async(messages, context_variables))
|
185
|
+
|
186
|
+
async def run_with_context_async(self, messages: List[Dict[str, str]], context_variables: dict) -> dict:
|
187
|
+
"""Asynchronously run the blueprint's logic."""
|
188
|
+
self.context_variables.update(context_variables)
|
189
|
+
logger.debug(f"Context variables updated: {self.context_variables}")
|
190
|
+
|
191
|
+
active_agent = await self.determine_active_agent()
|
192
|
+
if not active_agent:
|
193
|
+
logger.error("No active agent could be determined. Cannot proceed.")
|
194
|
+
return {"response": Response(messages=[{"role": "assistant", "content": "Error: No active agent available."}], agent=None, context_variables=self.context_variables), "context_variables": self.context_variables}
|
195
|
+
|
196
|
+
model = getattr(active_agent, 'model', None) or self.swarm.current_llm_config.get("model", "default")
|
197
|
+
logger.debug(f"Using model: {model} for agent {get_agent_name(active_agent)}")
|
198
|
+
|
199
|
+
truncated_messages = self.truncate_message_history(messages, model)
|
200
|
+
validated_messages = validate_message_sequence(truncated_messages)
|
201
|
+
repaired_messages = repair_message_payload(validated_messages, debug=self.debug)
|
202
|
+
|
203
|
+
if not self.swarm.agents:
|
204
|
+
logger.warning("No agents registered; returning default response.")
|
205
|
+
return {"response": Response(messages=[{"role": "assistant", "content": "No agents available in Swarm."}], agent=None, context_variables=self.context_variables), "context_variables": self.context_variables}
|
206
|
+
|
207
|
+
logger.debug(f"Running Swarm core with agent: {get_agent_name(active_agent)}")
|
208
|
+
self.spinner.start(f"Generating response from {get_agent_name(active_agent)}")
|
209
|
+
response_obj = None
|
210
|
+
try:
|
211
|
+
response_obj = await self.swarm.run(
|
212
|
+
agent=active_agent, messages=repaired_messages, context_variables=self.context_variables,
|
213
|
+
stream=False, debug=self.debug,
|
214
|
+
)
|
215
|
+
except Exception as e:
|
216
|
+
logger.error(f"Swarm run failed: {e}", exc_info=True)
|
217
|
+
response_obj = Response(messages=[{"role": "assistant", "content": f"An error occurred: {str(e)}"}], agent=active_agent, context_variables=self.context_variables)
|
218
|
+
finally:
|
219
|
+
self.spinner.stop()
|
220
|
+
|
221
|
+
final_agent = active_agent
|
222
|
+
updated_context = self.context_variables.copy()
|
223
|
+
|
224
|
+
if response_obj:
|
225
|
+
if hasattr(response_obj, 'agent') and response_obj.agent and get_agent_name(response_obj.agent) != get_agent_name(active_agent):
|
226
|
+
final_agent = response_obj.agent
|
227
|
+
new_agent_name = get_agent_name(final_agent)
|
228
|
+
updated_context["active_agent_name"] = new_agent_name
|
229
|
+
logger.debug(f"Agent handoff occurred. New active agent: {new_agent_name}")
|
230
|
+
# Corrected calls: Pass only agent and config
|
231
|
+
asyncio.ensure_future(discover_tools_for_agent(final_agent, self.swarm.config))
|
232
|
+
asyncio.ensure_future(discover_resources_for_agent(final_agent, self.swarm.config))
|
233
|
+
if hasattr(response_obj, 'context_variables'):
|
234
|
+
updated_context.update(response_obj.context_variables)
|
235
|
+
else:
|
236
|
+
logger.error("Swarm run returned None or invalid response structure.")
|
237
|
+
response_obj = Response(messages=[{"role": "assistant", "content": "Error processing the request."}], agent=active_agent, context_variables=updated_context)
|
238
|
+
|
239
|
+
return {"response": response_obj, "context_variables": updated_context}
|
240
|
+
|
241
|
+
def set_active_agent(self, agent_name: str) -> None:
|
242
|
+
"""Explicitly set the active agent by name and trigger asset discovery."""
|
243
|
+
if agent_name in self.swarm.agents:
|
244
|
+
self.context_variables["active_agent_name"] = agent_name
|
245
|
+
agent = self.swarm.agents[agent_name]
|
246
|
+
logger.debug(f"Explicitly setting active agent to: {agent_name}")
|
247
|
+
# Corrected calls: Pass only agent and config
|
248
|
+
if agent_name not in self._discovered_tools:
|
249
|
+
logger.debug(f"Discovering tools for explicitly set agent {agent_name}.")
|
250
|
+
try: asyncio.run(discover_tools_for_agent(agent, self.swarm.config))
|
251
|
+
except RuntimeError as e:
|
252
|
+
if "cannot be called from a running event loop" in str(e): logger.error("Cannot run sync discovery from within an async context (set_active_agent).")
|
253
|
+
else: raise e
|
254
|
+
if agent_name not in self._discovered_resources:
|
255
|
+
logger.debug(f"Discovering resources for explicitly set agent {agent_name}.")
|
256
|
+
try: asyncio.run(discover_resources_for_agent(agent, self.swarm.config))
|
257
|
+
except RuntimeError as e:
|
258
|
+
if "cannot be called from a running event loop" in str(e): logger.error("Cannot run sync discovery from within an async context (set_active_agent).")
|
259
|
+
else: raise e
|
260
|
+
else:
|
261
|
+
logger.error(f"Attempted to set active agent to '{agent_name}', but agent not found.")
|
262
|
+
|
263
|
+
# --- Task Completion & Goal Update Logic ---
|
264
|
+
async def _is_task_done_async(self, user_goal: str, conversation_summary: str, last_assistant_message: str) -> bool:
|
265
|
+
"""Check if the task defined by user_goal is complete using an LLM call."""
|
266
|
+
if not user_goal:
|
267
|
+
logger.warning("Cannot check task completion: user_goal is empty.")
|
268
|
+
return False
|
269
|
+
|
270
|
+
system_prompt = os.getenv("TASK_DONE_PROMPT", "You are a completion checker. Respond with ONLY 'YES' or 'NO'.")
|
271
|
+
user_prompt = os.getenv(
|
272
|
+
"TASK_DONE_USER_PROMPT",
|
273
|
+
"User's goal: {user_goal}\nConversation summary: {conversation_summary}\nLast assistant message: {last_assistant_message}\nIs the task fully complete? Answer only YES or NO."
|
274
|
+
).format(user_goal=user_goal, conversation_summary=conversation_summary, last_assistant_message=last_assistant_message)
|
275
|
+
|
276
|
+
check_prompt = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}]
|
277
|
+
client = self.swarm.client
|
278
|
+
model_to_use = self.swarm.current_llm_config.get("model", self.swarm.model)
|
279
|
+
|
280
|
+
try:
|
281
|
+
response = await client.chat.completions.create(
|
282
|
+
model=model_to_use, messages=check_prompt, max_tokens=5, temperature=0
|
283
|
+
)
|
284
|
+
if response.choices:
|
285
|
+
result_content = response.choices[0].message.content.strip().upper()
|
286
|
+
is_done = result_content.startswith("YES")
|
287
|
+
logger.debug(f"Task completion check (Goal: '{user_goal}', LLM Raw: '{result_content}'): {is_done}")
|
288
|
+
return is_done
|
289
|
+
else:
|
290
|
+
logger.warning("LLM response for task completion check had no choices.")
|
291
|
+
return False
|
292
|
+
except Exception as e:
|
293
|
+
logger.error(f"Task completion check LLM call failed: {e}", exc_info=True)
|
294
|
+
return False
|
295
|
+
|
296
|
+
async def _update_user_goal_async(self, messages: List[Dict[str, str]]) -> None:
|
297
|
+
"""Update the 'user_goal' in context_variables based on conversation history using an LLM call."""
|
298
|
+
if not messages:
|
299
|
+
logger.debug("Cannot update goal: No messages provided.")
|
300
|
+
return
|
301
|
+
|
302
|
+
system_prompt = os.getenv(
|
303
|
+
"UPDATE_GOAL_PROMPT",
|
304
|
+
"You are an assistant that summarizes the user's primary objective from the conversation. Provide a concise, one-sentence summary."
|
305
|
+
)
|
306
|
+
conversation_text = "\n".join(f"{m['role']}: {m.get('content', '')}" for m in messages if m.get('content') or m.get('tool_calls'))
|
307
|
+
if not conversation_text:
|
308
|
+
logger.debug("Cannot update goal: No content in messages.")
|
309
|
+
return
|
310
|
+
|
311
|
+
user_prompt = os.getenv(
|
312
|
+
"UPDATE_GOAL_USER_PROMPT",
|
313
|
+
"Summarize the user's main goal based on this conversation:\n{conversation}"
|
314
|
+
).format(conversation=conversation_text)
|
315
|
+
|
316
|
+
prompt = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}]
|
317
|
+
client = self.swarm.client
|
318
|
+
model_to_use = self.swarm.current_llm_config.get("model", self.swarm.model)
|
319
|
+
|
320
|
+
try:
|
321
|
+
response = await client.chat.completions.create(
|
322
|
+
model=model_to_use, messages=prompt, max_tokens=60, temperature=0.3
|
323
|
+
)
|
324
|
+
if response.choices:
|
325
|
+
new_goal = response.choices[0].message.content.strip()
|
326
|
+
if new_goal:
|
327
|
+
self.context_variables["user_goal"] = new_goal
|
328
|
+
logger.debug(f"Updated user goal via LLM: {new_goal}")
|
329
|
+
else:
|
330
|
+
logger.warning("LLM goal update returned empty response.")
|
331
|
+
else:
|
332
|
+
logger.warning("LLM response for goal update had no choices.")
|
333
|
+
except Exception as e:
|
334
|
+
logger.error(f"User goal update LLM call failed: {e}", exc_info=True)
|
335
|
+
|
336
|
+
def task_completed(self, outcome: str) -> None:
|
337
|
+
"""Placeholder method potentially used by agents to signal task completion."""
|
338
|
+
print(f"Task Outcome: {outcome}")
|
339
|
+
print("continue")
|
340
|
+
|
341
|
+
@property
|
342
|
+
def prompt(self) -> str:
|
343
|
+
"""Return the custom prompt string, potentially from the active agent."""
|
344
|
+
active_agent = self.swarm.agents.get(self.context_variables.get("active_agent_name"))
|
345
|
+
return getattr(active_agent, 'prompt', getattr(self, "custom_user_prompt", "User: "))
|
346
|
+
|
347
|
+
# --- Interactive & Non-Interactive Modes ---
|
348
|
+
def interactive_mode(self, stream: bool = False) -> None:
|
349
|
+
"""Run the blueprint in interactive command-line mode."""
|
350
|
+
from .interactive_mode import run_interactive_mode
|
351
|
+
run_interactive_mode(self, stream)
|
352
|
+
|
353
|
+
def non_interactive_mode(self, instruction: str, stream: bool = False) -> None:
|
354
|
+
"""Run the blueprint non-interactively with a single instruction."""
|
355
|
+
logger.debug(f"Starting non-interactive mode with instruction: {instruction}, stream={stream}")
|
356
|
+
try:
|
357
|
+
asyncio.run(self.non_interactive_mode_async(instruction, stream=stream))
|
358
|
+
except RuntimeError as e:
|
359
|
+
if "cannot be called from a running event loop" in str(e):
|
360
|
+
logger.error("Cannot start non_interactive_mode with asyncio.run from an existing event loop.")
|
361
|
+
else: raise e
|
362
|
+
|
363
|
+
async def non_interactive_mode_async(self, instruction: str, stream: bool = False) -> None:
|
364
|
+
"""Asynchronously run the blueprint non-interactively."""
|
365
|
+
logger.debug(f"Starting async non-interactive mode with instruction: {instruction}, stream={stream}")
|
366
|
+
if not self.swarm:
|
367
|
+
logger.error("Swarm instance not initialized.")
|
368
|
+
print("Error: Swarm framework not ready.")
|
369
|
+
return
|
370
|
+
|
371
|
+
print(f"--- {self.metadata.get('title', 'Blueprint')} Non-Interactive Mode ---")
|
372
|
+
instructions = [line.strip() for line in instruction.splitlines() if line.strip()]
|
373
|
+
if not instructions:
|
374
|
+
print("No valid instruction provided.")
|
375
|
+
return
|
376
|
+
messages = [{"role": "user", "content": line} for line in instructions]
|
377
|
+
|
378
|
+
if not self.starting_agent:
|
379
|
+
if self.swarm.agents:
|
380
|
+
first_agent_name = next(iter(self.swarm.agents.keys()))
|
381
|
+
logger.warning(f"No starting agent explicitly set. Defaulting to first agent: {first_agent_name}")
|
382
|
+
self.set_starting_agent(self.swarm.agents[first_agent_name])
|
383
|
+
else:
|
384
|
+
logger.error("No starting agent set and no agents defined.")
|
385
|
+
print("Error: No agent available to handle the instruction.")
|
386
|
+
return
|
387
|
+
|
388
|
+
self.context_variables["user_goal"] = instruction
|
389
|
+
self.context_variables["active_agent_name"] = get_agent_name(self.starting_agent)
|
390
|
+
|
391
|
+
if stream:
|
392
|
+
logger.debug("Running non-interactive in streaming mode.")
|
393
|
+
response_generator = self.swarm.run(
|
394
|
+
agent=self.starting_agent, messages=messages, context_variables=self.context_variables,
|
395
|
+
stream=True, debug=self.debug,
|
396
|
+
)
|
397
|
+
final_response_data = await self._process_and_print_streaming_response_async(response_generator)
|
398
|
+
if self.auto_complete_task:
|
399
|
+
logger.warning("Auto-completion is not fully supported with streaming in non-interactive mode.")
|
400
|
+
else:
|
401
|
+
logger.debug("Running non-interactive in non-streaming mode.")
|
402
|
+
result = await self.run_with_context_async(messages, self.context_variables)
|
403
|
+
swarm_response = result.get("response")
|
404
|
+
self.context_variables = result.get("context_variables", self.context_variables)
|
405
|
+
|
406
|
+
response_messages = []
|
407
|
+
if hasattr(swarm_response, 'messages'): response_messages = swarm_response.messages
|
408
|
+
elif isinstance(swarm_response, dict) and 'messages' in swarm_response: response_messages = swarm_response.get('messages', [])
|
409
|
+
|
410
|
+
self._pretty_print_response(response_messages)
|
411
|
+
if self.auto_complete_task and self.swarm.agents:
|
412
|
+
logger.debug("Starting auto-completion task.")
|
413
|
+
current_history = messages + response_messages
|
414
|
+
await self._auto_complete_task_async(current_history, stream=False)
|
415
|
+
|
416
|
+
print("--- Execution Completed ---")
|
417
|
+
|
418
|
+
async def _process_and_print_streaming_response_async(self, response_generator):
|
419
|
+
"""Async helper to process and print streaming response chunks."""
|
420
|
+
content = ""
|
421
|
+
last_sender = self.context_variables.get("active_agent_name", "Assistant")
|
422
|
+
final_response_chunk_data = None
|
423
|
+
try:
|
424
|
+
async for chunk in response_generator:
|
425
|
+
if isinstance(chunk, dict) and "delim" in chunk:
|
426
|
+
if chunk["delim"] == "start" and not content:
|
427
|
+
print(f"\033[94m{last_sender}\033[0m: ", end="", flush=True)
|
428
|
+
elif chunk["delim"] == "end" and content:
|
429
|
+
print()
|
430
|
+
content = ""
|
431
|
+
elif hasattr(chunk, 'choices') and chunk.choices:
|
432
|
+
delta = chunk.choices[0].delta
|
433
|
+
if delta and delta.content:
|
434
|
+
print(delta.content, end="", flush=True)
|
435
|
+
content += delta.content
|
436
|
+
elif isinstance(chunk, dict) and "response" in chunk:
|
437
|
+
final_response_chunk_data = chunk["response"]
|
438
|
+
if hasattr(final_response_chunk_data, 'agent'):
|
439
|
+
last_sender = get_agent_name(final_response_chunk_data.agent)
|
440
|
+
if hasattr(final_response_chunk_data, 'context_variables'):
|
441
|
+
self.context_variables.update(final_response_chunk_data.context_variables)
|
442
|
+
logger.debug("Received final aggregated response chunk in stream.")
|
443
|
+
elif isinstance(chunk, dict) and "error" in chunk:
|
444
|
+
logger.error(f"Error received during stream: {chunk['error']}")
|
445
|
+
print(f"\n[Stream Error: {chunk['error']}]")
|
446
|
+
except Exception as e:
|
447
|
+
logger.error(f"Error processing stream: {e}", exc_info=True)
|
448
|
+
print("\n[Error during streaming output]")
|
449
|
+
finally:
|
450
|
+
if content: print()
|
451
|
+
return final_response_chunk_data
|
452
|
+
|
453
|
+
async def _auto_complete_task_async(self, current_history: List[Dict[str, str]], stream: bool) -> None:
|
454
|
+
"""Async helper for task auto-completion loop (non-streaming)."""
|
455
|
+
max_auto_turns = 10
|
456
|
+
auto_turn = 0
|
457
|
+
while auto_turn < max_auto_turns:
|
458
|
+
auto_turn += 1
|
459
|
+
logger.debug(f"Auto-completion Turn: {auto_turn}/{max_auto_turns}")
|
460
|
+
conversation_summary = " ".join(m.get("content", "") for m in current_history[-4:] if m.get("content"))
|
461
|
+
last_assistant_msg = next((m.get("content", "") for m in reversed(current_history) if m.get("role") == "assistant" and m.get("content")), "")
|
462
|
+
user_goal = self.context_variables.get("user_goal", "")
|
463
|
+
|
464
|
+
# Call the renamed async method
|
465
|
+
if await self._is_task_done_async(user_goal, conversation_summary, last_assistant_msg):
|
466
|
+
print("\033[93m[System]\033[0m: Task detected as complete.")
|
467
|
+
break
|
468
|
+
|
469
|
+
logger.debug("Task not complete, running next auto-completion turn.")
|
470
|
+
result = await self.run_with_context_async(current_history, self.context_variables)
|
471
|
+
swarm_response = result.get("response")
|
472
|
+
self.context_variables = result.get("context_variables", self.context_variables)
|
473
|
+
|
474
|
+
new_messages = []
|
475
|
+
if hasattr(swarm_response, 'messages'): new_messages = swarm_response.messages
|
476
|
+
elif isinstance(swarm_response, dict) and 'messages' in swarm_response: new_messages = swarm_response.get('messages', [])
|
477
|
+
|
478
|
+
if not new_messages:
|
479
|
+
logger.warning("Auto-completion turn yielded no new messages. Stopping.")
|
480
|
+
break
|
481
|
+
|
482
|
+
self._pretty_print_response(new_messages)
|
483
|
+
current_history.extend(new_messages)
|
484
|
+
|
485
|
+
if auto_turn >= max_auto_turns:
|
486
|
+
logger.warning("Auto-completion reached maximum turns limit.")
|
487
|
+
print("\033[93m[System]\033[0m: Reached max auto-completion turns.")
|
488
|
+
|
489
|
+
def _auto_complete_task(self, messages: List[Dict[str, str]], stream: bool) -> None:
|
490
|
+
"""Synchronous wrapper for task auto-completion."""
|
491
|
+
if stream:
|
492
|
+
logger.warning("Auto-completion skipped because streaming is enabled.")
|
493
|
+
return
|
494
|
+
logger.debug("Starting synchronous auto-completion task.")
|
495
|
+
try:
|
496
|
+
asyncio.run(self._auto_complete_task_async(messages, stream=False))
|
497
|
+
except RuntimeError as e:
|
498
|
+
if "cannot be called from a running event loop" in str(e):
|
499
|
+
logger.error("Cannot start _auto_complete_task with asyncio.run from an existing event loop.")
|
500
|
+
else: raise e
|
501
|
+
|
502
|
+
# --- Class Method for Entry Point ---
|
503
|
+
@classmethod
|
504
|
+
def main(cls):
|
505
|
+
parser = argparse.ArgumentParser(description=f"Run the {cls.__name__} blueprint.")
|
506
|
+
parser.add_argument("--config", default="./swarm_config.json", help="Path to the swarm_config.json file.")
|
507
|
+
parser.add_argument("--instruction", help="Single instruction for non-interactive mode.")
|
508
|
+
parser.add_argument("--stream", action="store_true", help="Enable streaming output in non-interactive mode.")
|
509
|
+
parser.add_argument("--auto-complete-task", action="store_true", help="Enable task auto-completion in non-interactive mode.")
|
510
|
+
parser.add_argument("--update-user-goal", action="store_true", help="Enable dynamic goal updates using LLM.")
|
511
|
+
parser.add_argument("--update-user-goal-frequency", type=int, default=5, help="Frequency (in messages) for updating user goal.")
|
512
|
+
parser.add_argument("--log-file-path", help="Path for logging output (default: ~/.swarm/logs/<blueprint_name>.log).")
|
513
|
+
parser.add_argument("--debug", action="store_true", help="Enable debug logging to console instead of file.")
|
514
|
+
parser.add_argument("--use-markdown", action="store_true", help="Enable markdown rendering for assistant responses.")
|
515
|
+
args = parser.parse_args()
|
516
|
+
|
517
|
+
root_logger = logging.getLogger()
|
518
|
+
log_level = logging.DEBUG if args.debug or DEBUG else logging.INFO
|
519
|
+
root_logger.setLevel(log_level)
|
520
|
+
|
521
|
+
if root_logger.hasHandlers(): root_logger.handlers.clear()
|
522
|
+
|
523
|
+
log_formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(name)s:%(lineno)d - %(message)s")
|
524
|
+
log_handler = logging.StreamHandler(sys.stdout) if args.debug else logging.FileHandler(
|
525
|
+
Path(args.log_file_path or Path.home() / ".swarm" / "logs" / f"{cls.__name__.lower()}.log").resolve(), mode='a'
|
526
|
+
)
|
527
|
+
log_handler.setFormatter(log_formatter)
|
528
|
+
root_logger.addHandler(log_handler)
|
529
|
+
logger.info(f"Logging initialized. Level: {logging.getLevelName(log_level)}. Destination: {getattr(log_handler, 'baseFilename', 'console')}")
|
530
|
+
|
531
|
+
original_stderr = sys.stderr
|
532
|
+
dev_null = None
|
533
|
+
if not args.debug:
|
534
|
+
try:
|
535
|
+
dev_null = open(os.devnull, "w")
|
536
|
+
sys.stderr = dev_null
|
537
|
+
logger.info(f"Redirected stderr to {os.devnull}")
|
538
|
+
except OSError as e: logger.warning(f"Could not redirect stderr: {e}")
|
539
|
+
|
540
|
+
try:
|
541
|
+
config_data = load_server_config(args.config)
|
542
|
+
blueprint_instance = cls(
|
543
|
+
config=config_data, auto_complete_task=args.auto_complete_task, update_user_goal=args.update_user_goal,
|
544
|
+
update_user_goal_frequency=args.update_user_goal_frequency, log_file_path=str(getattr(log_handler, 'baseFilename', None)),
|
545
|
+
debug=args.debug, use_markdown=args.use_markdown, non_interactive=bool(args.instruction)
|
546
|
+
)
|
547
|
+
if args.instruction:
|
548
|
+
asyncio.run(blueprint_instance.non_interactive_mode_async(args.instruction, stream=args.stream))
|
549
|
+
else:
|
550
|
+
blueprint_instance.interactive_mode(stream=args.stream)
|
551
|
+
except Exception as e:
|
552
|
+
logger.critical(f"Blueprint execution failed: {e}", exc_info=True)
|
553
|
+
print(f"Critical Error: {e}", file=original_stderr)
|
554
|
+
finally:
|
555
|
+
if not args.debug and dev_null is not None:
|
556
|
+
sys.stderr = original_stderr
|
557
|
+
dev_null.close()
|
558
|
+
logger.debug("Restored stderr.")
|
559
|
+
logger.info("Blueprint execution finished.")
|
560
|
+
|
561
|
+
if __name__ == "__main__":
|
562
|
+
BlueprintBase.main()
|