open-swarm 0.1.1748636259__py3-none-any.whl → 0.1.1748636295__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,69 +1,41 @@
1
1
  """
2
- Swarm Blueprint Base Module (Sync Interactive Mode) - Updated to Use openai-agents
3
-
4
- This module provides the base class for blueprints with interactive and non-interactive modes.
5
- It has been refactored to use the openai-agents Runner for agent execution instead of the legacy Swarm core.
6
- Additionally, it initializes the mcp_servers attribute from the configuration and manages context variables.
7
- Since the original swarm.types module has been removed, minimal ChatMessage and Response classes are defined here.
2
+ Swarm Blueprint Base Module (Sync Interactive Mode)
8
3
  """
9
4
 
10
5
  import asyncio
11
6
  import json
12
7
  import logging
13
- from abc import ABC, abstractmethod
14
- from pathlib import Path
15
- from typing import Optional, Dict, Any, List
16
-
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
17
10
  import os
18
- import sys
19
11
  import uuid
12
+ import sys
13
+ from abc import ABC, abstractmethod
14
+ from typing import Optional, Dict, Any, List
20
15
 
16
+ from pathlib import Path
17
+ from swarm.core import Swarm
21
18
  from swarm.extensions.config.config_loader import load_server_config
22
19
  from swarm.settings import DEBUG
23
20
  from swarm.utils.redact import redact_sensitive_data
24
- from swarm.utils.message_sequence import repair_message_payload, validate_message_sequence
25
- from swarm.utils.context_utils import truncate_message_history
26
- from swarm.extensions.blueprint.agent_utils import get_agent_name, initialize_agents
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
+ )
27
28
  from swarm.extensions.blueprint.django_utils import register_django_components
28
29
  from swarm.extensions.blueprint.spinner import Spinner
30
+ from swarm.extensions.blueprint.output_utils import pretty_print_response
29
31
  from dotenv import load_dotenv
30
32
  import argparse
31
-
32
- class DummyMCPServer:
33
- async def list_tools(self) -> list:
34
- return []
35
-
36
- # Import Runner and Agent from the openai-agents SDK.
37
- from agents import Runner # type: ignore
38
- from agents.agent import Agent # type: ignore
39
-
40
- # Minimal definitions to replace swarm.types
41
- from dataclasses import dataclass
42
-
43
- @dataclass
44
- class ChatMessage:
45
- role: str
46
- content: str
47
-
48
- @dataclass
49
- class Response:
50
- messages: List[ChatMessage]
51
- agent: Optional[Any]
52
- context_variables: Dict[str, Any]
33
+ from swarm.types import Agent, Response
53
34
 
54
35
  logger = logging.getLogger(__name__)
55
36
 
56
37
  class BlueprintBase(ABC):
57
- """
58
- Base class for blueprints using the openai-agents Runner for execution.
59
- Agents are expected to be created via create_agents() and stored in self.agents.
60
- Runner.run() is used to execute agents with a plain text input.
61
-
62
- This version initializes mcp_servers from the configuration and restores context_variables.
63
- """
64
-
65
- # Set up initial context variables as a class variable.
66
- context_variables: Dict[str, Any] = {"user_goal": ""}
38
+ """Base class for Swarm blueprints with sync interactive mode and Django integration."""
67
39
 
68
40
  def __init__(
69
41
  self,
@@ -87,372 +59,504 @@ class BlueprintBase(ABC):
87
59
  self.log_file_path = log_file_path
88
60
  self.debug = debug or DEBUG
89
61
  self.use_markdown = use_markdown
90
- self._urls_registered = False # For Django URL registration
62
+ self._urls_registered = False
91
63
 
92
64
  if self.use_markdown:
93
- logger.debug("Markdown rendering enabled.")
65
+ logger.debug("Markdown rendering enabled (if rich is available).")
94
66
  logger.debug(f"Initializing {self.__class__.__name__} with config: {redact_sensitive_data(config)}")
95
- if not hasattr(self, "metadata") or not isinstance(self.metadata, dict):
96
- try:
97
- _ = self.metadata
98
- if not isinstance(self.metadata, dict):
99
- raise TypeError("Metadata is not a dict")
100
- except (AttributeError, NotImplementedError, TypeError) as e:
101
- raise AssertionError(f"{self.__class__.__name__} must define a 'metadata' property returning a dictionary. Error: {e}")
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.")
102
69
 
103
70
  self.truncation_mode = os.getenv("SWARM_TRUNCATION_MODE", "pairs").lower()
104
- meta = self.metadata
105
- self.max_context_tokens = max(1, meta.get("max_context_tokens", 8000))
106
- self.max_context_messages = max(1, meta.get("max_context_messages", 50))
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))
107
73
  logger.debug(f"Truncation settings: mode={self.truncation_mode}, max_tokens={self.max_context_tokens}, max_messages={self.max_context_messages}")
108
74
 
109
75
  load_dotenv()
110
- logger.debug("Loaded environment variables from .env (if present).")
76
+ logger.debug("Loaded environment variables from .env.")
111
77
 
112
78
  self.config = config
113
- # Initialize mcp_servers from configuration.
114
- self.mcp_servers: Dict[str, Any] = {}
115
- if "mcp_servers" in self.config:
116
- self.mcp_servers = load_server_config(self.config["mcp_servers"])
117
- logger.debug(f"Loaded mcp_servers: {list(self.mcp_servers.keys())}")
118
- else:
119
- logger.debug("No mcp_servers configuration found.")
120
-
121
- # Set default mcp server configurations if keys are missing.
122
- if "mcp_llms_txt_server" not in self.mcp_servers:
123
- logger.warning("mcp_llms_txt_server not found in mcp_servers; using default dummy configuration.")
124
- self.mcp_servers["mcp_llms_txt_server"] = {"command": "echo", "args": [], "env": {}}
125
- if "everything_server" not in self.mcp_servers:
126
- logger.warning("everything_server not found in mcp_servers; using default dummy configuration.")
127
- self.mcp_servers["everything_server"] = {"command": "echo", "args": [], "env": {}}
128
-
129
79
  self.skip_django_registration = skip_django_registration or not os.environ.get("DJANGO_SETTINGS_MODULE")
130
- # Initialize agents.
131
- initialized_agents = initialize_agents(self) # type: ignore
132
- self.agents: Dict[str, Agent] = initialized_agents if initialized_agents is not None else {}
133
- # Restore context_variables on the instance.
134
- self.context_variables = {"user_goal": ""}
135
- self.starting_agent: Optional[Agent] = None
80
+ self.swarm = kwargs.get('swarm_instance') or Swarm(config=self.config, debug=self.debug)
81
+ logger.debug("Swarm instance initialized.")
136
82
 
83
+ self.context_variables: Dict[str, Any] = {"user_goal": ""}
84
+ self.starting_agent = None
137
85
  self._discovered_tools: Dict[str, List[Any]] = {}
138
86
  self._discovered_resources: Dict[str, List[Any]] = {}
139
- self.spinner = Spinner(interactive=not kwargs.get("non_interactive", False))
87
+ self.spinner = Spinner(interactive=not kwargs.get('non_interactive', False))
140
88
 
141
- required_env_vars = set(meta.get("env_vars", []))
89
+ required_env_vars = set(self.metadata.get('env_vars', []))
142
90
  missing_vars = [var for var in required_env_vars if not os.getenv(var)]
143
91
  if missing_vars:
144
- logger.warning(f"Missing environment variables for {meta.get('title', self.__class__.__name__)}: {', '.join(missing_vars)}")
92
+ logger.warning(f"Missing environment variables for {self.metadata.get('title', self.__class__.__name__)}: {', '.join(missing_vars)}")
145
93
 
146
- self.required_mcp_servers = meta.get("required_mcp_servers", [])
147
- logger.debug(f"Required MCP servers from metadata: {self.required_mcp_servers}")
94
+ self.required_mcp_servers = self.metadata.get('required_mcp_servers', [])
95
+ logger.debug(f"Required MCP servers: {self.required_mcp_servers}")
148
96
 
149
97
  if self._is_create_agents_overridden():
150
- initialized_agents = initialize_agents(self) # type: ignore
151
- self.agents = initialized_agents if initialized_agents is not None else {}
98
+ initialize_agents(self)
152
99
  register_django_components(self)
153
100
 
154
101
  def _is_create_agents_overridden(self) -> bool:
155
- return getattr(self.__class__, "create_agents") is not getattr(BlueprintBase, "create_agents")
102
+ """Check if the 'create_agents' method is overridden in the subclass."""
103
+ return self.__class__.create_agents is not BlueprintBase.create_agents
156
104
 
157
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."""
158
107
  return truncate_message_history(messages, model, self.max_context_tokens, self.max_context_messages)
159
108
 
160
109
  @property
161
110
  @abstractmethod
162
111
  def metadata(self) -> Dict[str, Any]:
163
- raise NotImplementedError("Subclasses must implement the 'metadata' property.")
112
+ """Abstract property for blueprint metadata."""
113
+ raise NotImplementedError
164
114
 
165
115
  def create_agents(self) -> Dict[str, Agent]:
166
- logger.debug(f"{self.__class__.__name__} using default create_agents (returns empty dict). Override if agents are needed.")
116
+ """Default agent creation method."""
167
117
  return {}
168
118
 
169
119
  def set_starting_agent(self, agent: Agent) -> None:
170
- agent_name = get_agent_name(agent) # type: ignore
120
+ """Set the starting agent and trigger initial asset discovery."""
121
+ agent_name = get_agent_name(agent)
171
122
  logger.debug(f"Setting starting agent to: {agent_name}")
172
123
  self.starting_agent = agent
173
- # Only track the active agent if handoffs are defined (indicating async runner usage).
174
- if hasattr(agent, "handoffs"):
175
- self.context_variables["active_agent_name"] = agent_name
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
176
146
 
177
147
  async def determine_active_agent(self) -> Optional[Agent]:
148
+ """Determine the currently active agent."""
178
149
  active_agent_name = self.context_variables.get("active_agent_name")
179
- if active_agent_name and active_agent_name in self.agents:
180
- logger.debug(f"Active agent determined: {active_agent_name}")
181
- return self.agents[active_agent_name]
182
- elif self.starting_agent is not None:
183
- agent_to_use = self.starting_agent
184
- starting_agent_name = get_agent_name(agent_to_use) # type: ignore
185
- if active_agent_name != starting_agent_name:
186
- logger.warning(f"Active agent name '{active_agent_name}' invalid; defaulting to starting agent: {starting_agent_name}")
187
- self.context_variables["active_agent_name"] = starting_agent_name
188
- else:
189
- logger.debug(f"Using starting agent: {starting_agent_name}")
190
- return agent_to_use
191
- elif self.agents:
192
- first_agent_name = next(iter(self.agents))
193
- logger.warning(f"No active agent set. Defaulting to first registered agent: {first_agent_name}")
194
- self.context_variables["active_agent_name"] = first_agent_name
195
- return self.agents[first_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}")
196
163
  else:
197
- logger.error("No agents registered in blueprint.")
198
- return None
199
-
200
- def run_with_context(self, messages: List[Dict[str, Any]], context_variables: dict) -> dict:
201
- dict_messages = []
202
- for msg in messages:
203
- if hasattr(msg, "model_dump"):
204
- dict_messages.append(msg.model_dump(exclude_none=True)) # type: ignore
205
- elif isinstance(msg, dict):
206
- dict_messages.append(msg)
207
- else:
208
- logger.warning(f"Skipping non-dict message: {type(msg)}")
209
- continue
210
- return asyncio.run(self.run_with_context_async(dict_messages, context_variables))
211
-
212
- async def run_with_context_async(self, messages: List[Dict[str, Any]], context_variables: dict) -> dict:
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."""
213
188
  self.context_variables.update(context_variables)
214
- logger.debug(f"Context variables updated: {list(self.context_variables.keys())}")
189
+ logger.debug(f"Context variables updated: {self.context_variables}")
190
+
215
191
  active_agent = await self.determine_active_agent()
216
192
  if not active_agent:
217
- logger.error("No active agent available.")
218
- error_msg = ChatMessage(role="assistant", content="Error: No active agent available.")
219
- return {
220
- "response": Response(messages=[error_msg], agent=None, context_variables=self.context_variables),
221
- "context_variables": self.context_variables
222
- }
223
- input_text = " ".join(msg.get("content", "") for msg in messages if "content" in msg)
224
- if not input_text:
225
- logger.warning("No valid input found in messages.")
226
- input_text = ""
227
- logger.debug(f"Running Runner with agent {get_agent_name(active_agent)} and input: {input_text[:100]}...")
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)}")
228
208
  self.spinner.start(f"Generating response from {get_agent_name(active_agent)}")
209
+ response_obj = None
229
210
  try:
230
- result = await Runner.run(active_agent, input_text) # type: ignore
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
+ )
231
215
  except Exception as e:
232
- logger.error(f"Runner.run failed: {e}", exc_info=True)
233
- error_msg = ChatMessage(role="assistant", content=f"Error: {str(e)}")
234
- result = Response(messages=[error_msg], agent=active_agent, context_variables=self.context_variables)
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)
235
218
  finally:
236
219
  self.spinner.stop()
220
+
221
+ final_agent = active_agent
237
222
  updated_context = self.context_variables.copy()
238
- return {"response": result, "context_variables": updated_context}
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}
239
240
 
240
241
  def set_active_agent(self, agent_name: str) -> None:
241
- if agent_name in self.agents:
242
+ """Explicitly set the active agent by name and trigger asset discovery."""
243
+ if agent_name in self.swarm.agents:
242
244
  self.context_variables["active_agent_name"] = agent_name
243
- logger.debug(f"Active agent set to: {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
244
260
  else:
245
- logger.error(f"Agent '{agent_name}' not found. Available: {list(self.agents.keys())}")
261
+ logger.error(f"Attempted to set active agent to '{agent_name}', but agent not found.")
246
262
 
263
+ # --- Task Completion & Goal Update Logic ---
247
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."""
248
266
  if not user_goal:
249
- logger.warning("Empty user_goal; cannot check task completion.")
250
- return False
251
- system_prompt = os.getenv("TASK_DONE_PROMPT", "You are a completion checker. Answer ONLY YES or NO.")
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'.")
252
271
  user_prompt = os.getenv(
253
272
  "TASK_DONE_USER_PROMPT",
254
- "User's goal: {user_goal}\nConversation summary: {conversation_summary}\nLast assistant message: {last_assistant_message}\nIs the task complete? Answer only YES or NO."
255
- ).format(
256
- user_goal=user_goal,
257
- conversation_summary=conversation_summary,
258
- last_assistant_message=last_assistant_message
259
- )
260
- check_prompt = [
261
- {"role": "system", "content": system_prompt},
262
- {"role": "user", "content": user_prompt}
263
- ]
264
- client = Runner.client # type: ignore
265
- model_to_use = Runner.current_llm_config.get("model", Runner.model) # type: ignore
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
+
266
280
  try:
267
- if not client:
268
- raise ValueError("Runner client not available.")
269
281
  response = await client.chat.completions.create(
270
- model=model_to_use,
271
- messages=check_prompt,
272
- max_tokens=5,
273
- temperature=0
282
+ model=model_to_use, messages=check_prompt, max_tokens=5, temperature=0
274
283
  )
275
284
  if response.choices:
276
- result_content = response.choices[0].message.content.strip().upper()
277
- is_done = result_content.startswith("YES")
278
- logger.debug(f"Task completion check: {is_done} (raw: '{result_content}')")
279
- return is_done
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
280
289
  else:
281
- logger.warning("No choices in LLM response for task completion check.")
282
- return False
290
+ logger.warning("LLM response for task completion check had no choices.")
291
+ return False
283
292
  except Exception as e:
284
- logger.error(f"Task completion check failed: {e}", exc_info=True)
293
+ logger.error(f"Task completion check LLM call failed: {e}", exc_info=True)
285
294
  return False
286
295
 
287
- async def _update_user_goal_async(self, messages: List[Dict[str, Any]]) -> None:
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."""
288
298
  if not messages:
289
- logger.debug("No messages provided for goal update.")
299
+ logger.debug("Cannot update goal: No messages provided.")
290
300
  return
301
+
291
302
  system_prompt = os.getenv(
292
303
  "UPDATE_GOAL_PROMPT",
293
- "Summarize the user's primary objective from the conversation in one sentence."
294
- )
295
- conversation_text = "\n".join(
296
- f"{m.get('sender', m.get('role', ''))}: {m.get('content', '') or '[Tool Call]'}"
297
- for m in messages if m.get("content") or m.get("tool_calls")
304
+ "You are an assistant that summarizes the user's primary objective from the conversation. Provide a concise, one-sentence summary."
298
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'))
299
307
  if not conversation_text:
300
- logger.debug("No usable conversation content for goal update.")
301
- return
308
+ logger.debug("Cannot update goal: No content in messages.")
309
+ return
310
+
302
311
  user_prompt = os.getenv(
303
312
  "UPDATE_GOAL_USER_PROMPT",
304
313
  "Summarize the user's main goal based on this conversation:\n{conversation}"
305
- ).format(conversation=conversation_text[-2000:])
306
- prompt = [
307
- {"role": "system", "content": system_prompt},
308
- {"role": "user", "content": user_prompt}
309
- ]
310
- client = Runner.client # type: ignore
311
- model_to_use = Runner.current_llm_config.get("model", Runner.model) # type: ignore
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
+
312
320
  try:
313
- if not client:
314
- raise ValueError("Runner client not available for goal update.")
315
321
  response = await client.chat.completions.create(
316
- model=model_to_use,
317
- messages=prompt,
318
- max_tokens=60,
319
- temperature=0.3
322
+ model=model_to_use, messages=prompt, max_tokens=60, temperature=0.3
320
323
  )
321
324
  if response.choices:
322
- new_goal = response.choices[0].message.content.strip()
323
- if new_goal and new_goal != self.context_variables.get("user_goal"):
324
- self.context_variables["user_goal"] = new_goal
325
- logger.info(f"Updated user goal: {new_goal}")
326
- elif not new_goal:
327
- logger.warning("LLM returned an empty goal for update.")
328
- else:
329
- logger.debug("LLM goal update produced the same goal.")
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.")
330
331
  else:
331
- logger.warning("No choices in LLM response for goal update.")
332
+ logger.warning("LLM response for goal update had no choices.")
332
333
  except Exception as e:
333
- logger.error(f"Goal update failed: {e}", exc_info=True)
334
+ logger.error(f"User goal update LLM call failed: {e}", exc_info=True)
334
335
 
335
336
  def task_completed(self, outcome: str) -> None:
336
- print(f"\n\033[93m[System Task Outcome]\033[0m: {outcome}")
337
+ """Placeholder method potentially used by agents to signal task completion."""
338
+ print(f"Task Outcome: {outcome}")
339
+ print("continue")
337
340
 
338
341
  @property
339
342
  def prompt(self) -> str:
340
- active_agent_name = self.context_variables.get("active_agent_name")
341
- active_agent = self.agents.get(active_agent_name) if active_agent_name else None
342
- if active_agent:
343
- return f"{get_agent_name(active_agent)} > "
344
- else:
345
- return "User: "
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
346
 
347
+ # --- Interactive & Non-Interactive Modes ---
347
348
  def interactive_mode(self, stream: bool = False) -> None:
348
- try:
349
- from .interactive_mode import run_interactive_mode
350
- run_interactive_mode(self, stream)
351
- except ImportError:
352
- logger.critical("Failed to import interactive_mode runner.")
353
- print("Error: Cannot start interactive mode.", file=sys.stderr)
349
+ """Run the blueprint in interactive command-line mode."""
350
+ from .interactive_mode import run_interactive_mode
351
+ run_interactive_mode(self, stream)
354
352
 
355
353
  def non_interactive_mode(self, instruction: str, stream: bool = False) -> None:
354
+ """Run the blueprint non-interactively with a single instruction."""
356
355
  logger.debug(f"Starting non-interactive mode with instruction: {instruction}, stream={stream}")
357
356
  try:
358
- asyncio.run(self.non_interactive_mode_async(instruction, stream=stream))
357
+ asyncio.run(self.non_interactive_mode_async(instruction, stream=stream))
359
358
  except RuntimeError as e:
360
- if "cannot be called from a running event loop" in str(e):
361
- logger.error("Non-interactive mode cannot run within an async context.")
362
- print("Error: Non-interactive mode cannot run within an async context.", file=sys.stderr)
363
- else:
364
- raise e
365
- except Exception as e:
366
- logger.error(f"Error during non-interactive mode: {e}", exc_info=True)
367
- print(f"Error: {e}", file=sys.stderr)
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
368
362
 
369
363
  async def non_interactive_mode_async(self, instruction: str, stream: bool = False) -> None:
364
+ """Asynchronously run the blueprint non-interactively."""
370
365
  logger.debug(f"Starting async non-interactive mode with instruction: {instruction}, stream={stream}")
371
- if not self.agents:
372
- logger.error("No agents available in blueprint instance.")
373
- print("Error: No agents available.", file=sys.stderr)
366
+ if not self.swarm:
367
+ logger.error("Swarm instance not initialized.")
368
+ print("Error: Swarm framework not ready.")
374
369
  return
375
370
 
376
371
  print(f"--- {self.metadata.get('title', 'Blueprint')} Non-Interactive Mode ---")
377
372
  instructions = [line.strip() for line in instruction.splitlines() if line.strip()]
378
373
  if not instructions:
379
- print("No valid instruction provided.")
380
- return
374
+ print("No valid instruction provided.")
375
+ return
376
+ messages = [{"role": "user", "content": line} for line in instructions]
381
377
 
382
- messages: List[Dict[str, Any]] = [{"role": "user", "content": line} for line in instructions]
383
378
  if not self.starting_agent:
384
- if self.agents:
385
- first_agent_name = next(iter(self.agents.keys()))
386
- logger.warning(f"No starting agent set. Defaulting to first agent: {first_agent_name}")
387
- self.set_starting_agent(self.agents[first_agent_name])
388
- else:
389
- logger.error("No starting agent set and no agents defined.")
390
- print("Error: No agent available.", file=sys.stderr)
391
- return
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
392
387
 
393
388
  self.context_variables["user_goal"] = instruction
394
- if "active_agent_name" not in self.context_variables:
395
- self.context_variables["active_agent_name"] = get_agent_name(self.starting_agent) # type: ignore
389
+ self.context_variables["active_agent_name"] = get_agent_name(self.starting_agent)
396
390
 
397
391
  if stream:
398
392
  logger.debug("Running non-interactive in streaming mode.")
399
- active_agent = await self.determine_active_agent()
400
- if not active_agent:
401
- return
402
- response_generator = Runner.run(active_agent, " ".join(m.get("content", "") for m in messages)) # type: ignore
403
- await self._process_and_print_streaming_response_async(response_generator)
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)
404
398
  if self.auto_complete_task:
405
- logger.warning("Auto-completion with streaming is not fully supported.")
399
+ logger.warning("Auto-completion is not fully supported with streaming in non-interactive mode.")
406
400
  else:
407
401
  logger.debug("Running non-interactive in non-streaming mode.")
408
- input_text = " ".join(m.get("content", "") for m in messages)
409
- result = await Runner.run(self.starting_agent, input_text) # type: ignore
410
- if hasattr(result, "final_output"):
411
- print(f"\nFinal response:\n{result.final_output}")
412
- else:
413
- print("Received unexpected response format.")
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
+
414
416
  print("--- Execution Completed ---")
415
417
 
416
- async def _process_and_print_streaming_response_async(self, response_generator) -> Optional[Dict[str, Any]]:
417
- full_content = ""
418
- async for chunk in response_generator:
419
- if "error" in chunk:
420
- logger.error(f"Streaming error: {chunk['error']}")
421
- print(f"Error: {chunk['error']}", file=sys.stderr)
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.")
422
467
  break
423
- if "choices" in chunk:
424
- for choice in chunk["choices"]:
425
- delta = choice.get("delta", {}).get("content", "")
426
- full_content += delta
427
- print(delta, end="", flush=True)
428
- print()
429
- return None
430
-
431
- async def _auto_complete_task_async(self, current_history: List[Dict[str, Any]], stream: bool) -> None:
432
- logger.debug("Auto-complete task not implemented.")
433
- pass
434
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 ---
435
503
  @classmethod
436
504
  def main(cls):
437
505
  parser = argparse.ArgumentParser(description=f"Run the {cls.__name__} blueprint.")
438
- parser.add_argument("--config", default="./swarm_config.json", help="Path to the configuration file.")
439
- parser.add_argument("--instruction", help="Instruction for non-interactive mode.")
440
- parser.add_argument("--stream", action="store_true", help="Enable streaming mode.")
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.")
441
515
  args = parser.parse_args()
442
- if not os.path.exists(args.config):
443
- logger.error(f"Configuration file {args.config} not found.")
444
- sys.exit(1)
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
+
445
540
  try:
446
- with open(args.config, "r") as f:
447
- config = json.load(f)
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)
448
551
  except Exception as e:
449
- logger.error(f"Failed to load configuration: {e}")
450
- sys.exit(1)
451
- blueprint_instance = cls(config=config)
452
- if args.instruction:
453
- blueprint_instance.non_interactive_mode(instruction=args.instruction, stream=args.stream)
454
- else:
455
- blueprint_instance.interactive_mode(stream=args.stream)
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.")
456
560
 
457
561
  if __name__ == "__main__":
458
562
  BlueprintBase.main()