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.
- open_swarm-0.1.1748636295.dist-info/METADATA +257 -0
- {open_swarm-0.1.1748636259.dist-info → open_swarm-0.1.1748636295.dist-info}/RECORD +24 -17
- swarm/__init__.py +2 -0
- swarm/core.py +411 -0
- swarm/extensions/blueprint/agent_utils.py +40 -16
- swarm/extensions/blueprint/blueprint_base.py +394 -290
- swarm/extensions/blueprint/django_utils.py +79 -181
- swarm/extensions/blueprint/interactive_mode.py +72 -67
- swarm/extensions/blueprint/output_utils.py +22 -35
- swarm/extensions/config/config_loader.py +253 -109
- swarm/extensions/mcp/__init__.py +1 -0
- swarm/extensions/mcp/cache_utils.py +32 -0
- swarm/extensions/mcp/mcp_client.py +233 -0
- swarm/extensions/mcp/mcp_tool_provider.py +135 -0
- swarm/extensions/mcp/mcp_utils.py +260 -0
- swarm/llm/chat_completion.py +19 -48
- swarm/settings.py +106 -70
- swarm/types.py +91 -0
- swarm/views/chat_views.py +38 -31
- swarm/views/utils.py +1 -3
- open_swarm-0.1.1748636259.dist-info/METADATA +0 -188
- {open_swarm-0.1.1748636259.dist-info → open_swarm-0.1.1748636295.dist-info}/WHEEL +0 -0
- {open_swarm-0.1.1748636259.dist-info → open_swarm-0.1.1748636295.dist-info}/entry_points.txt +0 -0
- {open_swarm-0.1.1748636259.dist-info → open_swarm-0.1.1748636295.dist-info}/licenses/LICENSE +0 -0
- {open_swarm-0.1.1748636259.dist-info → open_swarm-0.1.1748636295.dist-info}/top_level.txt +0 -0
@@ -1,69 +1,41 @@
|
|
1
1
|
"""
|
2
|
-
Swarm Blueprint Base Module (Sync Interactive Mode)
|
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
|
14
|
-
from
|
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.
|
25
|
-
from swarm.
|
26
|
-
|
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
|
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,
|
96
|
-
|
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
|
-
|
105
|
-
self.
|
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
|
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
|
-
|
131
|
-
|
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(
|
87
|
+
self.spinner = Spinner(interactive=not kwargs.get('non_interactive', False))
|
140
88
|
|
141
|
-
required_env_vars = set(
|
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 {
|
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 =
|
147
|
-
logger.debug(f"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
|
-
|
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
|
-
|
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
|
-
|
112
|
+
"""Abstract property for blueprint metadata."""
|
113
|
+
raise NotImplementedError
|
164
114
|
|
165
115
|
def create_agents(self) -> Dict[str, Agent]:
|
166
|
-
|
116
|
+
"""Default agent creation method."""
|
167
117
|
return {}
|
168
118
|
|
169
119
|
def set_starting_agent(self, agent: Agent) -> None:
|
170
|
-
|
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
|
-
|
174
|
-
|
175
|
-
|
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
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
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
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
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: {
|
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
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
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
|
-
|
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"
|
233
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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"
|
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
|
-
|
250
|
-
|
251
|
-
|
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
|
-
|
257
|
-
|
258
|
-
|
259
|
-
)
|
260
|
-
|
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
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
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
|
-
|
282
|
-
|
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,
|
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
|
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
|
-
"
|
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
|
-
|
301
|
-
|
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
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
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
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
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
|
-
|
332
|
+
logger.warning("LLM response for goal update had no choices.")
|
332
333
|
except Exception as e:
|
333
|
-
logger.error(f"
|
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
|
-
|
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
|
-
|
341
|
-
active_agent = self.agents.get(active_agent_name)
|
342
|
-
|
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
|
-
|
349
|
-
|
350
|
-
|
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
|
-
|
357
|
+
asyncio.run(self.non_interactive_mode_async(instruction, stream=stream))
|
359
358
|
except RuntimeError as e:
|
360
|
-
|
361
|
-
|
362
|
-
|
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.
|
372
|
-
logger.error("
|
373
|
-
print("Error:
|
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
|
-
|
380
|
-
|
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
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
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
|
-
|
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
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
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
|
-
|
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
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
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)
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
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
|
439
|
-
parser.add_argument("--instruction", help="
|
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
|
-
|
443
|
-
|
444
|
-
|
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
|
-
|
447
|
-
|
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
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
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()
|