open-swarm 0.1.1743070217__py3-none-any.whl → 0.1.1743364176__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.1743364176.dist-info/METADATA +286 -0
- open_swarm-0.1.1743364176.dist-info/RECORD +260 -0
- {open_swarm-0.1.1743070217.dist-info → open_swarm-0.1.1743364176.dist-info}/WHEEL +1 -2
- open_swarm-0.1.1743364176.dist-info/entry_points.txt +2 -0
- swarm/__init__.py +0 -2
- swarm/auth.py +53 -49
- swarm/blueprints/README.md +67 -0
- swarm/blueprints/burnt_noodles/blueprint_burnt_noodles.py +412 -0
- swarm/blueprints/chatbot/blueprint_chatbot.py +98 -0
- swarm/blueprints/chatbot/templates/chatbot/chatbot.html +33 -0
- swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py +183 -0
- swarm/blueprints/dilbot_universe/blueprint_dilbot_universe.py +285 -0
- swarm/blueprints/divine_code/__init__.py +0 -0
- swarm/blueprints/divine_code/apps.py +11 -0
- swarm/blueprints/divine_code/blueprint_divine_code.py +219 -0
- swarm/blueprints/django_chat/apps.py +6 -0
- swarm/blueprints/django_chat/blueprint_django_chat.py +84 -0
- swarm/blueprints/django_chat/templates/django_chat/django_chat_webpage.html +37 -0
- swarm/blueprints/django_chat/urls.py +8 -0
- swarm/blueprints/django_chat/views.py +32 -0
- swarm/blueprints/echocraft/blueprint_echocraft.py +44 -0
- swarm/blueprints/family_ties/apps.py +11 -0
- swarm/blueprints/family_ties/blueprint_family_ties.py +152 -0
- swarm/blueprints/family_ties/models.py +19 -0
- swarm/blueprints/family_ties/serializers.py +7 -0
- swarm/blueprints/family_ties/settings.py +16 -0
- swarm/blueprints/family_ties/urls.py +10 -0
- swarm/blueprints/family_ties/views.py +26 -0
- swarm/blueprints/flock/__init__.py +0 -0
- swarm/blueprints/gaggle/blueprint_gaggle.py +184 -0
- swarm/blueprints/gotchaman/blueprint_gotchaman.py +232 -0
- swarm/blueprints/mcp_demo/blueprint_mcp_demo.py +133 -0
- swarm/blueprints/messenger/templates/messenger/messenger.html +46 -0
- swarm/blueprints/mission_improbable/blueprint_mission_improbable.py +234 -0
- swarm/blueprints/monkai_magic/blueprint_monkai_magic.py +248 -0
- swarm/blueprints/nebula_shellz/blueprint_nebula_shellz.py +156 -0
- swarm/blueprints/omniplex/blueprint_omniplex.py +221 -0
- swarm/blueprints/rue_code/__init__.py +0 -0
- swarm/blueprints/rue_code/blueprint_rue_code.py +291 -0
- swarm/blueprints/suggestion/blueprint_suggestion.py +110 -0
- swarm/blueprints/unapologetic_press/blueprint_unapologetic_press.py +298 -0
- swarm/blueprints/whiskeytango_foxtrot/__init__.py +0 -0
- swarm/blueprints/whiskeytango_foxtrot/apps.py +11 -0
- swarm/blueprints/whiskeytango_foxtrot/blueprint_whiskeytango_foxtrot.py +256 -0
- swarm/extensions/blueprint/__init__.py +30 -15
- swarm/extensions/blueprint/agent_utils.py +16 -40
- swarm/extensions/blueprint/blueprint_base.py +141 -543
- swarm/extensions/blueprint/blueprint_discovery.py +112 -98
- swarm/extensions/blueprint/cli_handler.py +185 -0
- swarm/extensions/blueprint/config_loader.py +122 -0
- swarm/extensions/blueprint/django_utils.py +181 -79
- swarm/extensions/blueprint/interactive_mode.py +1 -1
- swarm/extensions/config/config_loader.py +83 -200
- swarm/extensions/launchers/build_swarm_wrapper.py +0 -0
- swarm/extensions/launchers/swarm_cli.py +199 -287
- swarm/llm/chat_completion.py +26 -55
- swarm/management/__init__.py +0 -0
- swarm/management/commands/__init__.py +0 -0
- swarm/management/commands/runserver.py +58 -0
- swarm/permissions.py +38 -0
- swarm/serializers.py +96 -5
- swarm/settings.py +95 -110
- swarm/static/contrib/fonts/fontawesome-webfont.ttf +7 -0
- swarm/static/contrib/fonts/fontawesome-webfont.woff +7 -0
- swarm/static/contrib/fonts/fontawesome-webfont.woff2 +7 -0
- swarm/static/contrib/markedjs/marked.min.js +6 -0
- swarm/static/contrib/tabler-icons/adjustments-horizontal.svg +27 -0
- swarm/static/contrib/tabler-icons/alert-triangle.svg +21 -0
- swarm/static/contrib/tabler-icons/archive.svg +21 -0
- swarm/static/contrib/tabler-icons/artboard.svg +27 -0
- swarm/static/contrib/tabler-icons/automatic-gearbox.svg +23 -0
- swarm/static/contrib/tabler-icons/box-multiple.svg +19 -0
- swarm/static/contrib/tabler-icons/carambola.svg +19 -0
- swarm/static/contrib/tabler-icons/copy.svg +20 -0
- swarm/static/contrib/tabler-icons/download.svg +21 -0
- swarm/static/contrib/tabler-icons/edit.svg +21 -0
- swarm/static/contrib/tabler-icons/filled/carambola.svg +13 -0
- swarm/static/contrib/tabler-icons/filled/paint.svg +13 -0
- swarm/static/contrib/tabler-icons/headset.svg +22 -0
- swarm/static/contrib/tabler-icons/layout-sidebar-left-collapse.svg +21 -0
- swarm/static/contrib/tabler-icons/layout-sidebar-left-expand.svg +21 -0
- swarm/static/contrib/tabler-icons/layout-sidebar-right-collapse.svg +21 -0
- swarm/static/contrib/tabler-icons/layout-sidebar-right-expand.svg +21 -0
- swarm/static/contrib/tabler-icons/message-chatbot.svg +22 -0
- swarm/static/contrib/tabler-icons/message-star.svg +22 -0
- swarm/static/contrib/tabler-icons/message-x.svg +23 -0
- swarm/static/contrib/tabler-icons/message.svg +21 -0
- swarm/static/contrib/tabler-icons/paperclip.svg +18 -0
- swarm/static/contrib/tabler-icons/playlist-add.svg +22 -0
- swarm/static/contrib/tabler-icons/robot.svg +26 -0
- swarm/static/contrib/tabler-icons/search.svg +19 -0
- swarm/static/contrib/tabler-icons/settings.svg +20 -0
- swarm/static/contrib/tabler-icons/thumb-down.svg +19 -0
- swarm/static/contrib/tabler-icons/thumb-up.svg +19 -0
- swarm/static/css/dropdown.css +22 -0
- swarm/static/htmx/htmx.min.js +0 -0
- swarm/static/js/dropdown.js +23 -0
- swarm/static/rest_mode/css/base.css +470 -0
- swarm/static/rest_mode/css/chat-history.css +286 -0
- swarm/static/rest_mode/css/chat.css +251 -0
- swarm/static/rest_mode/css/chatbot.css +74 -0
- swarm/static/rest_mode/css/chatgpt.css +62 -0
- swarm/static/rest_mode/css/colors/corporate.css +74 -0
- swarm/static/rest_mode/css/colors/pastel.css +81 -0
- swarm/static/rest_mode/css/colors/tropical.css +82 -0
- swarm/static/rest_mode/css/general.css +142 -0
- swarm/static/rest_mode/css/layout.css +167 -0
- swarm/static/rest_mode/css/layouts/messenger-layout.css +17 -0
- swarm/static/rest_mode/css/layouts/minimalist-layout.css +57 -0
- swarm/static/rest_mode/css/layouts/mobile-layout.css +8 -0
- swarm/static/rest_mode/css/messages.css +84 -0
- swarm/static/rest_mode/css/messenger.css +135 -0
- swarm/static/rest_mode/css/settings.css +91 -0
- swarm/static/rest_mode/css/simple.css +44 -0
- swarm/static/rest_mode/css/slack.css +58 -0
- swarm/static/rest_mode/css/style.css +156 -0
- swarm/static/rest_mode/css/theme.css +30 -0
- swarm/static/rest_mode/css/toast.css +40 -0
- swarm/static/rest_mode/js/auth.js +9 -0
- swarm/static/rest_mode/js/blueprint.js +41 -0
- swarm/static/rest_mode/js/blueprintUtils.js +12 -0
- swarm/static/rest_mode/js/chatLogic.js +79 -0
- swarm/static/rest_mode/js/debug.js +63 -0
- swarm/static/rest_mode/js/events.js +98 -0
- swarm/static/rest_mode/js/main.js +19 -0
- swarm/static/rest_mode/js/messages.js +264 -0
- swarm/static/rest_mode/js/messengerLogic.js +355 -0
- swarm/static/rest_mode/js/modules/apiService.js +84 -0
- swarm/static/rest_mode/js/modules/blueprintManager.js +162 -0
- swarm/static/rest_mode/js/modules/chatHistory.js +110 -0
- swarm/static/rest_mode/js/modules/debugLogger.js +14 -0
- swarm/static/rest_mode/js/modules/eventHandlers.js +107 -0
- swarm/static/rest_mode/js/modules/messageProcessor.js +120 -0
- swarm/static/rest_mode/js/modules/state.js +7 -0
- swarm/static/rest_mode/js/modules/userInteractions.js +29 -0
- swarm/static/rest_mode/js/modules/validation.js +23 -0
- swarm/static/rest_mode/js/rendering.js +119 -0
- swarm/static/rest_mode/js/settings.js +130 -0
- swarm/static/rest_mode/js/sidebar.js +94 -0
- swarm/static/rest_mode/js/simpleLogic.js +37 -0
- swarm/static/rest_mode/js/slackLogic.js +66 -0
- swarm/static/rest_mode/js/splash.js +76 -0
- swarm/static/rest_mode/js/theme.js +111 -0
- swarm/static/rest_mode/js/toast.js +36 -0
- swarm/static/rest_mode/js/ui.js +265 -0
- swarm/static/rest_mode/js/validation.js +57 -0
- swarm/static/rest_mode/svg/animated_spinner.svg +12 -0
- swarm/static/rest_mode/svg/arrow_down.svg +5 -0
- swarm/static/rest_mode/svg/arrow_left.svg +5 -0
- swarm/static/rest_mode/svg/arrow_right.svg +5 -0
- swarm/static/rest_mode/svg/arrow_up.svg +5 -0
- swarm/static/rest_mode/svg/attach.svg +8 -0
- swarm/static/rest_mode/svg/avatar.svg +7 -0
- swarm/static/rest_mode/svg/canvas.svg +6 -0
- swarm/static/rest_mode/svg/chat_history.svg +4 -0
- swarm/static/rest_mode/svg/close.svg +5 -0
- swarm/static/rest_mode/svg/copy.svg +4 -0
- swarm/static/rest_mode/svg/dark_mode.svg +3 -0
- swarm/static/rest_mode/svg/edit.svg +5 -0
- swarm/static/rest_mode/svg/layout.svg +9 -0
- swarm/static/rest_mode/svg/logo.svg +29 -0
- swarm/static/rest_mode/svg/logout.svg +5 -0
- swarm/static/rest_mode/svg/mobile.svg +5 -0
- swarm/static/rest_mode/svg/new_chat.svg +4 -0
- swarm/static/rest_mode/svg/not_visible.svg +5 -0
- swarm/static/rest_mode/svg/plus.svg +7 -0
- swarm/static/rest_mode/svg/run_code.svg +6 -0
- swarm/static/rest_mode/svg/save.svg +4 -0
- swarm/static/rest_mode/svg/search.svg +6 -0
- swarm/static/rest_mode/svg/settings.svg +4 -0
- swarm/static/rest_mode/svg/speaker.svg +5 -0
- swarm/static/rest_mode/svg/stop.svg +6 -0
- swarm/static/rest_mode/svg/thumbs_down.svg +3 -0
- swarm/static/rest_mode/svg/thumbs_up.svg +3 -0
- swarm/static/rest_mode/svg/toggle_off.svg +6 -0
- swarm/static/rest_mode/svg/toggle_on.svg +6 -0
- swarm/static/rest_mode/svg/trash.svg +10 -0
- swarm/static/rest_mode/svg/undo.svg +3 -0
- swarm/static/rest_mode/svg/visible.svg +8 -0
- swarm/static/rest_mode/svg/voice.svg +10 -0
- swarm/templates/account/login.html +22 -0
- swarm/templates/account/signup.html +32 -0
- swarm/templates/base.html +30 -0
- swarm/templates/chat.html +43 -0
- swarm/templates/index.html +35 -0
- swarm/templates/rest_mode/components/chat_sidebar.html +55 -0
- swarm/templates/rest_mode/components/header.html +45 -0
- swarm/templates/rest_mode/components/main_chat_pane.html +41 -0
- swarm/templates/rest_mode/components/settings_dialog.html +97 -0
- swarm/templates/rest_mode/components/splash_screen.html +7 -0
- swarm/templates/rest_mode/components/top_bar.html +28 -0
- swarm/templates/rest_mode/message_ui.html +50 -0
- swarm/templates/rest_mode/slackbot.html +30 -0
- swarm/templates/simple_blueprint_page.html +24 -0
- swarm/templates/websocket_partials/final_system_message.html +3 -0
- swarm/templates/websocket_partials/system_message.html +4 -0
- swarm/templates/websocket_partials/user_message.html +5 -0
- swarm/urls.py +57 -74
- swarm/utils/log_utils.py +63 -0
- swarm/views/api_views.py +48 -39
- swarm/views/chat_views.py +156 -70
- swarm/views/core_views.py +85 -90
- swarm/views/model_views.py +64 -121
- swarm/views/utils.py +65 -441
- open_swarm-0.1.1743070217.dist-info/METADATA +0 -258
- open_swarm-0.1.1743070217.dist-info/RECORD +0 -89
- open_swarm-0.1.1743070217.dist-info/entry_points.txt +0 -3
- open_swarm-0.1.1743070217.dist-info/top_level.txt +0 -1
- swarm/agent/agent.py +0 -49
- swarm/core.py +0 -326
- swarm/extensions/mcp/__init__.py +0 -1
- swarm/extensions/mcp/cache_utils.py +0 -36
- swarm/extensions/mcp/mcp_client.py +0 -341
- swarm/extensions/mcp/mcp_constants.py +0 -7
- swarm/extensions/mcp/mcp_tool_provider.py +0 -110
- swarm/types.py +0 -126
- {open_swarm-0.1.1743070217.dist-info → open_swarm-0.1.1743364176.dist-info}/licenses/LICENSE +0 -0
swarm/core.py
DELETED
@@ -1,326 +0,0 @@
|
|
1
|
-
import os
|
2
|
-
import json
|
3
|
-
import logging
|
4
|
-
import asyncio
|
5
|
-
import re # Import re for tool name validation in provider
|
6
|
-
from typing import List, Dict, Optional, Union, AsyncGenerator, Any, Callable
|
7
|
-
from openai import AsyncOpenAI, OpenAIError
|
8
|
-
import uuid
|
9
|
-
|
10
|
-
from .types import Agent, LLMConfig, Response, ToolCall, ToolResult, ChatMessage, Tool
|
11
|
-
from .settings import Settings
|
12
|
-
from .extensions.config.config_loader import load_server_config, load_llm_config, get_server_params # Import load_server_config
|
13
|
-
from .utils.redact import redact_sensitive_data
|
14
|
-
from .llm.chat_completion import get_chat_completion_message
|
15
|
-
from .extensions.mcp.mcp_tool_provider import MCPToolProvider
|
16
|
-
from .utils.context_utils import get_token_count
|
17
|
-
|
18
|
-
settings = Settings()
|
19
|
-
logger = logging.getLogger(__name__)
|
20
|
-
logger.setLevel(settings.log_level.upper())
|
21
|
-
if not logger.handlers and not logging.getLogger().handlers:
|
22
|
-
log_handler = logging.StreamHandler()
|
23
|
-
formatter = logging.Formatter(settings.log_format.value)
|
24
|
-
log_handler.setFormatter(formatter)
|
25
|
-
logger.addHandler(log_handler)
|
26
|
-
|
27
|
-
logger.debug(f"Swarm Core initialized with log level: {settings.log_level.upper()}")
|
28
|
-
|
29
|
-
# --- FIX: Define correct separator ---
|
30
|
-
MCP_TOOL_SEPARATOR = "__"
|
31
|
-
|
32
|
-
# --- Helper Function: Discover and Merge Agent Tools ---
|
33
|
-
async def discover_and_merge_agent_tools(agent: Agent, config: Dict[str, Any], timeout: int, debug: bool) -> List[Tool]:
|
34
|
-
"""
|
35
|
-
Discovers tools from MCP servers listed in agent.mcp_servers and merges
|
36
|
-
them with the agent's static functions. Returns a list of Tool objects.
|
37
|
-
"""
|
38
|
-
merged_tools: Dict[str, Tool] = {}
|
39
|
-
|
40
|
-
# 1. Process static functions
|
41
|
-
if hasattr(agent, 'functions') and agent.functions:
|
42
|
-
for func in agent.functions:
|
43
|
-
if isinstance(func, Tool):
|
44
|
-
if func.name in merged_tools: logger.warning(f"Duplicate tool name '{func.name}'. Overwriting.")
|
45
|
-
merged_tools[func.name] = func
|
46
|
-
elif callable(func):
|
47
|
-
tool_name = getattr(func, '__name__', f'callable_{uuid.uuid4().hex[:6]}')
|
48
|
-
if not re.match(r"^[a-zA-Z0-9_-]{1,64}$", tool_name): # Validate static tool name
|
49
|
-
logger.warning(f"Static function name '{tool_name}' violates OpenAI pattern. Skipping.")
|
50
|
-
continue
|
51
|
-
if tool_name in merged_tools: logger.warning(f"Duplicate static tool name '{tool_name}'. Overwriting.")
|
52
|
-
|
53
|
-
docstring = getattr(func, '__doc__', None)
|
54
|
-
description = docstring.strip() if docstring else f"Executes the {tool_name} function."
|
55
|
-
|
56
|
-
input_schema = {"type": "object", "properties": {}}
|
57
|
-
merged_tools[tool_name] = Tool(name=tool_name, func=func, description=description, input_schema=input_schema)
|
58
|
-
else: logger.warning(f"Ignoring non-callable item in agent functions list: {func}")
|
59
|
-
logger.debug(f"Agent '{agent.name}': Processed {len(merged_tools)} static tools.")
|
60
|
-
|
61
|
-
# 2. Discover tools from MCP servers
|
62
|
-
if agent.mcp_servers:
|
63
|
-
mcp_server_configs = config.get("mcpServers", {})
|
64
|
-
discovery_tasks = []
|
65
|
-
for server_name in agent.mcp_servers:
|
66
|
-
if server_name not in mcp_server_configs:
|
67
|
-
logger.warning(f"Config for MCP server '{server_name}' for agent '{agent.name}' not found. Skipping.")
|
68
|
-
continue
|
69
|
-
server_config = mcp_server_configs[server_name]
|
70
|
-
if not get_server_params(server_config, server_name):
|
71
|
-
logger.error(f"Invalid config for MCP server '{server_name}'. Cannot discover.")
|
72
|
-
continue
|
73
|
-
try:
|
74
|
-
provider = MCPToolProvider.get_instance(server_name=server_name, server_config=server_config, timeout=timeout, debug=debug)
|
75
|
-
if provider.client: discovery_tasks.append(provider.discover_tools(agent))
|
76
|
-
else: logger.error(f"MCPClient failed init for '{server_name}'.")
|
77
|
-
except Exception as e: logger.error(f"Error getting MCP instance for '{server_name}': {e}", exc_info=True)
|
78
|
-
|
79
|
-
if discovery_tasks:
|
80
|
-
logger.debug(f"Awaiting discovery from {len(discovery_tasks)} MCP providers.")
|
81
|
-
results = await asyncio.gather(*discovery_tasks, return_exceptions=True)
|
82
|
-
for result in results:
|
83
|
-
if isinstance(result, Exception): logger.error(f"MCP discovery error: {result}")
|
84
|
-
elif isinstance(result, list):
|
85
|
-
for mcp_tool in result:
|
86
|
-
if mcp_tool.name in merged_tools: logger.warning(f"Duplicate tool name '{mcp_tool.name}' (MCP vs Static/Other MCP). Overwriting.")
|
87
|
-
merged_tools[mcp_tool.name] = mcp_tool # Name already prefixed by provider
|
88
|
-
else: logger.warning(f"Unexpected result type during MCP discovery: {type(result)}")
|
89
|
-
|
90
|
-
final_tool_list = list(merged_tools.values())
|
91
|
-
logger.info(f"Agent '{agent.name}': Final merged tool count: {len(final_tool_list)}")
|
92
|
-
if debug: logger.debug(f"Agent '{agent.name}': Final tools: {[t.name for t in final_tool_list]}")
|
93
|
-
return final_tool_list
|
94
|
-
|
95
|
-
# --- Helper Function: Format Tools for LLM ---
|
96
|
-
def format_tools_for_llm(tools: List[Tool]) -> List[Dict[str, Any]]:
|
97
|
-
"""Formats the Tool list into the structure expected by OpenAI API."""
|
98
|
-
if not tools: return []
|
99
|
-
formatted = []
|
100
|
-
for tool in tools:
|
101
|
-
parameters = tool.input_schema or {"type": "object", "properties": {}}
|
102
|
-
if not isinstance(parameters, dict) or "type" not in parameters:
|
103
|
-
logger.warning(f"Invalid schema for tool '{tool.name}'. Using default. Schema: {parameters}")
|
104
|
-
parameters = {"type": "object", "properties": {}}
|
105
|
-
elif parameters.get("type") == "object" and "properties" not in parameters:
|
106
|
-
parameters["properties"] = {}
|
107
|
-
|
108
|
-
# Validate tool name again before formatting
|
109
|
-
if not re.match(r"^[a-zA-Z0-9_-]{1,64}$", tool.name):
|
110
|
-
logger.error(f"Tool name '{tool.name}' is invalid for OpenAI API. Skipping.")
|
111
|
-
continue
|
112
|
-
|
113
|
-
formatted.append({
|
114
|
-
"type": "function",
|
115
|
-
"function": {
|
116
|
-
"name": tool.name,
|
117
|
-
"description": tool.description or f"Executes the {tool.name} tool.",
|
118
|
-
"parameters": parameters,
|
119
|
-
},
|
120
|
-
})
|
121
|
-
return formatted
|
122
|
-
|
123
|
-
# --- Swarm Class ---
|
124
|
-
class Swarm:
|
125
|
-
def __init__(
|
126
|
-
self,
|
127
|
-
llm_profile: str = "default",
|
128
|
-
config: Optional[dict] = None,
|
129
|
-
api_key: Optional[str] = None,
|
130
|
-
base_url: Optional[str] = None,
|
131
|
-
model: Optional[str] = None,
|
132
|
-
agents: Optional[Dict[str, Agent]] = None,
|
133
|
-
max_context_tokens: int = 8000,
|
134
|
-
max_context_messages: int = 50,
|
135
|
-
max_tool_response_tokens: int = 4096,
|
136
|
-
max_total_tool_response_tokens: int = 16384,
|
137
|
-
max_tool_calls_per_turn: int = 10,
|
138
|
-
tool_execution_timeout: int = 120,
|
139
|
-
tool_discovery_timeout: int = 15,
|
140
|
-
debug: bool = False,
|
141
|
-
):
|
142
|
-
self.debug = debug or settings.debug
|
143
|
-
if self.debug: logger.setLevel(logging.DEBUG); [h.setLevel(logging.DEBUG) for h in logging.getLogger().handlers if hasattr(h, 'setLevel')] ; logger.debug("Debug mode enabled.")
|
144
|
-
self.tool_execution_timeout = tool_execution_timeout
|
145
|
-
self.tool_discovery_timeout = tool_discovery_timeout
|
146
|
-
self.agents = agents or {}; logger.debug(f"Initial agents: {list(self.agents.keys())}")
|
147
|
-
# Load config if not provided
|
148
|
-
self.config = config if config is not None else load_server_config()
|
149
|
-
logger.debug(f"INIT START: Received api_key arg: {'****' if api_key else 'None'}")
|
150
|
-
|
151
|
-
llm_profile_name = os.getenv("DEFAULT_LLM", llm_profile)
|
152
|
-
logger.debug(f"INIT: Using LLM profile name: '{llm_profile_name}'")
|
153
|
-
try:
|
154
|
-
loaded_config_dict = load_llm_config(self.config, llm_profile_name)
|
155
|
-
except Exception as e: logger.critical(f"INIT: Failed to load config for profile '{llm_profile_name}': {e}", exc_info=True); raise
|
156
|
-
|
157
|
-
final_config = loaded_config_dict.copy(); log_key_source = final_config.get("_log_key_source", "load_llm_config")
|
158
|
-
if api_key is not None: final_config['api_key'] = api_key; log_key_source = "__init__ arg"
|
159
|
-
if base_url is not None: final_config['base_url'] = base_url
|
160
|
-
if model is not None: final_config['model'] = model
|
161
|
-
self.current_llm_config = final_config; self.model = self.current_llm_config.get("model"); self.provider = self.current_llm_config.get("provider")
|
162
|
-
|
163
|
-
self.max_context_tokens=max_context_tokens; self.max_context_messages=max_context_messages
|
164
|
-
self.max_tool_response_tokens=max_tool_response_tokens; self.max_total_tool_response_tokens=max_total_tool_response_tokens
|
165
|
-
self.max_tool_calls_per_turn=max_tool_calls_per_turn
|
166
|
-
|
167
|
-
client_kwargs = {"api_key": self.current_llm_config.get("api_key"), "base_url": self.current_llm_config.get("base_url")}
|
168
|
-
client_kwargs = {k: v for k, v in client_kwargs.items() if v is not None}
|
169
|
-
try:
|
170
|
-
self.client = AsyncOpenAI(**client_kwargs)
|
171
|
-
final_api_key_used = self.current_llm_config.get("api_key")
|
172
|
-
logger.info(f"Swarm initialized. LLM Profile: '{llm_profile_name}', Model: '{self.model}', Key Source: {log_key_source}, Key Used: {'****' if final_api_key_used else 'None'}")
|
173
|
-
if self.debug: logger.debug(f"AsyncOpenAI client kwargs: {redact_sensitive_data(client_kwargs)}")
|
174
|
-
except Exception as e: logger.critical(f"Failed to initialize OpenAI client: {e}", exc_info=True); raise
|
175
|
-
self._agent_tools: Dict[str, List[Tool]] = {}
|
176
|
-
|
177
|
-
def register_agent(self, agent: Agent):
|
178
|
-
if agent.name in self.agents: logger.warning(f"Agent '{agent.name}' already registered. Overwriting.")
|
179
|
-
self.agents[agent.name] = agent; logger.info(f"Agent '{agent.name}' registered.")
|
180
|
-
if agent.name in self._agent_tools: del self._agent_tools[agent.name]
|
181
|
-
if self.debug: logger.debug(f"Agent details: {agent}")
|
182
|
-
|
183
|
-
async def _get_agent_tools(self, agent: Agent) -> List[Tool]:
|
184
|
-
if agent.name not in self._agent_tools:
|
185
|
-
logger.debug(f"Tools cache miss for agent '{agent.name}'. Discovering...")
|
186
|
-
self._agent_tools[agent.name] = await discover_and_merge_agent_tools(agent, self.config, self.tool_discovery_timeout, self.debug)
|
187
|
-
return self._agent_tools[agent.name]
|
188
|
-
|
189
|
-
async def _execute_tool_call(self, agent: Agent, tool_call: ToolCall, context_variables: Dict[str, Any]) -> ToolResult:
|
190
|
-
"""Executes a single tool call, handling static and MCP tools."""
|
191
|
-
function_name = tool_call.function.name # This is the name LLM used (could be prefixed)
|
192
|
-
tool_call_id = tool_call.id
|
193
|
-
logger.info(f"Executing tool call '{function_name}' (ID: {tool_call_id}) for agent '{agent.name}'.")
|
194
|
-
arguments = {}
|
195
|
-
content = f"Error: Tool '{function_name}' execution failed."
|
196
|
-
|
197
|
-
try:
|
198
|
-
args_raw = tool_call.function.arguments
|
199
|
-
arguments = json.loads(args_raw) if isinstance(args_raw, str) else args_raw
|
200
|
-
if not isinstance(arguments, dict):
|
201
|
-
logger.error(f"Parsed tool args for {function_name} not dict: {type(arguments)}. Args: {args_raw}")
|
202
|
-
raise ValueError("Tool arguments must be a JSON object.")
|
203
|
-
except json.JSONDecodeError as e:
|
204
|
-
logger.error(f"JSONDecodeError parsing args for {function_name}: {e}. Args: {args_raw}")
|
205
|
-
content = f"Error: Invalid JSON args for '{function_name}': {e}"
|
206
|
-
except ValueError as e: # Catch the explicit error from above
|
207
|
-
content = str(e)
|
208
|
-
except Exception as e:
|
209
|
-
logger.error(f"Error processing args for {function_name}: {e}", exc_info=True)
|
210
|
-
content = f"Error processing args for '{function_name}'."
|
211
|
-
|
212
|
-
tool_executed = False
|
213
|
-
if isinstance(arguments, dict): # Proceed only if args are valid
|
214
|
-
agent_tools = await self._get_agent_tools(agent)
|
215
|
-
target_tool: Optional[Tool] = next((t for t in agent_tools if t.name == function_name), None)
|
216
|
-
|
217
|
-
if target_tool and callable(target_tool.func):
|
218
|
-
tool_executed = True
|
219
|
-
logger.debug(f"Found tool '{function_name}'. Executing...")
|
220
|
-
try:
|
221
|
-
if asyncio.iscoroutinefunction(target_tool.func):
|
222
|
-
result = await asyncio.wait_for(target_tool.func(**arguments), timeout=self.tool_execution_timeout)
|
223
|
-
else:
|
224
|
-
# Consider running sync functions in threadpool executor
|
225
|
-
result = target_tool.func(**arguments)
|
226
|
-
|
227
|
-
# Process result
|
228
|
-
if isinstance(result, Agent):
|
229
|
-
logger.info(f"Handoff signal: Result is Agent '{result.name}'.")
|
230
|
-
# --- FIX: Use correct separator ---
|
231
|
-
content = f"HANDOFF{MCP_TOOL_SEPARATOR}{result.name}"
|
232
|
-
elif isinstance(result, (dict, list, tuple)): content = json.dumps(result, default=str)
|
233
|
-
elif result is None: content = "Tool executed successfully with no return value."
|
234
|
-
else: content = str(result)
|
235
|
-
logger.debug(f"Tool '{function_name}' executed. Raw result type: {type(result)}")
|
236
|
-
|
237
|
-
except asyncio.TimeoutError:
|
238
|
-
logger.error(f"Timeout executing tool '{function_name}'.")
|
239
|
-
content = f"Error: Tool '{function_name}' timed out ({self.tool_execution_timeout}s)."
|
240
|
-
except Exception as e:
|
241
|
-
logger.error(f"Error executing tool {function_name}: {e}", exc_info=True)
|
242
|
-
content = f"Error: Tool '{function_name}' failed: {e}"
|
243
|
-
# else: Tool not found error handled below
|
244
|
-
|
245
|
-
if not tool_executed and isinstance(arguments, dict):
|
246
|
-
logger.error(f"Tool '{function_name}' not found for agent '{agent.name}'. Available: {[t.name for t in await self._get_agent_tools(agent)]}")
|
247
|
-
content = f"Error: Tool '{function_name}' not available for agent '{agent.name}'."
|
248
|
-
|
249
|
-
# Truncation
|
250
|
-
# --- FIX: Use correct separator ---
|
251
|
-
if isinstance(content, str) and not content.startswith(f"HANDOFF{MCP_TOOL_SEPARATOR}"):
|
252
|
-
token_count = get_token_count(content, self.current_llm_config.get("model"))
|
253
|
-
if token_count > self.max_tool_response_tokens:
|
254
|
-
logger.warning(f"Truncating tool response '{function_name}'. Size: {token_count} > Limit: {self.max_tool_response_tokens}")
|
255
|
-
content = content[:self.max_tool_response_tokens * 4] + "... (truncated)"
|
256
|
-
|
257
|
-
return ToolResult(tool_call_id=tool_call_id, name=function_name, content=content)
|
258
|
-
|
259
|
-
async def _run_non_streaming(self, agent: Agent, messages: List[Dict[str, Any]], context_variables: Optional[Dict[str, Any]] = None, max_turns: int = 10, debug: bool = False) -> Response:
|
260
|
-
current_agent = agent; history = list(messages); context_vars = context_variables.copy() if context_variables else {}; turn = 0
|
261
|
-
while turn < max_turns:
|
262
|
-
turn += 1; logger.debug(f"Turn {turn} starting with agent '{current_agent.name}'.")
|
263
|
-
agent_tools = await self._get_agent_tools(current_agent); formatted_tools = format_tools_for_llm(agent_tools)
|
264
|
-
if debug and formatted_tools: logger.debug(f"Tools for '{current_agent.name}': {[t['function']['name'] for t in formatted_tools]}")
|
265
|
-
try:
|
266
|
-
ai_message_dict = await get_chat_completion_message(client=self.client, agent=current_agent, history=history, context_variables=context_vars, current_llm_config=self.current_llm_config, max_context_tokens=self.max_context_tokens, max_context_messages=self.max_context_messages, tools=formatted_tools or None, tool_choice="auto" if formatted_tools else None, stream=False, debug=debug)
|
267
|
-
ai_message_dict["sender"] = current_agent.name; history.append(ai_message_dict)
|
268
|
-
tool_calls_raw = ai_message_dict.get("tool_calls")
|
269
|
-
if tool_calls_raw:
|
270
|
-
if not isinstance(tool_calls_raw, list): tool_calls_raw = []
|
271
|
-
logger.info(f"Agent '{current_agent.name}' requested {len(tool_calls_raw)} tool calls.")
|
272
|
-
tool_calls_to_execute = []
|
273
|
-
for tc_raw in tool_calls_raw[:self.max_tool_calls_per_turn]:
|
274
|
-
try:
|
275
|
-
if isinstance(tc_raw, dict) and 'function' in tc_raw and isinstance(tc_raw['function'], dict) and 'name' in tc_raw['function'] and 'arguments' in tc_raw['function']: tool_calls_to_execute.append(ToolCall(**tc_raw))
|
276
|
-
else: logger.warning(f"Skipping malformed tool call: {tc_raw}")
|
277
|
-
except Exception as p_err: logger.warning(f"Skipping tool call validation error: {p_err}. Raw: {tc_raw}")
|
278
|
-
if len(tool_calls_raw) > self.max_tool_calls_per_turn: logger.warning(f"Clamping tool calls to {self.max_tool_calls_per_turn}.")
|
279
|
-
|
280
|
-
tool_tasks = [self._execute_tool_call(current_agent, tc, context_vars) for tc in tool_calls_to_execute]
|
281
|
-
tool_results: List[ToolResult] = await asyncio.gather(*tool_tasks)
|
282
|
-
next_agent_name_from_handoff = None; total_tool_response_tokens = 0
|
283
|
-
for result in tool_results:
|
284
|
-
history.append(result.model_dump(exclude_none=True)); content = result.content
|
285
|
-
if isinstance(content, str):
|
286
|
-
# --- FIX: Use correct separator ---
|
287
|
-
if content.startswith(f"HANDOFF{MCP_TOOL_SEPARATOR}"):
|
288
|
-
parts = content.split(MCP_TOOL_SEPARATOR, 1); potential_next_agent = parts[1] if len(parts) > 1 else None
|
289
|
-
if potential_next_agent and potential_next_agent in self.agents:
|
290
|
-
if not next_agent_name_from_handoff: next_agent_name_from_handoff = potential_next_agent; logger.info(f"Handoff to '{next_agent_name_from_handoff}' confirmed.")
|
291
|
-
elif next_agent_name_from_handoff != potential_next_agent: logger.warning(f"Multiple handoffs requested. Using first '{next_agent_name_from_handoff}'.")
|
292
|
-
else: logger.warning(f"Handoff to unknown agent '{potential_next_agent}'. Ignoring.")
|
293
|
-
else: total_tool_response_tokens += get_token_count(content, self.current_llm_config.get("model"))
|
294
|
-
if total_tool_response_tokens > self.max_total_tool_response_tokens: logger.error(f"Total tool tokens ({total_tool_response_tokens}) exceeded limit. Ending run."); history.append({"role": "assistant", "sender": "System", "content": "[System Error: Tool responses token limit exceeded.]"}); break
|
295
|
-
if next_agent_name_from_handoff: current_agent = self.agents[next_agent_name_from_handoff]; context_vars["active_agent_name"] = current_agent.name; logger.debug(f"Activating agent '{current_agent.name}'."); continue
|
296
|
-
else: continue
|
297
|
-
else: break # No tool calls, end interaction
|
298
|
-
except OpenAIError as e: logger.error(f"API error turn {turn} for '{current_agent.name}': {e}", exc_info=True); history.append({"role": "assistant", "sender": "System", "content": f"[System Error: API call failed]"}); break
|
299
|
-
except Exception as e: logger.error(f"Unexpected error turn {turn} for '{current_agent.name}': {e}", exc_info=True); history.append({"role": "assistant", "sender": "System", "content": f"[System Error: Unexpected error]"}); break
|
300
|
-
if turn >= max_turns: logger.warning(f"Reached max turns ({max_turns}).")
|
301
|
-
logger.debug(f"Non-streaming run completed. Turns={turn}, History Messages={len(history)}.")
|
302
|
-
final_messages_raw = history[len(messages):]; final_messages_typed = [ChatMessage(**msg) for msg in final_messages_raw if isinstance(msg, dict)]
|
303
|
-
response_id = f"response-{uuid.uuid4()}"
|
304
|
-
return Response(id=response_id, messages=final_messages_typed, agent=current_agent, context_variables=context_vars)
|
305
|
-
|
306
|
-
async def _run_streaming(self, agent: Agent, messages: List[Dict[str, Any]], context_variables: Optional[Dict[str, Any]] = None, max_turns: int = 10, debug: bool = False) -> AsyncGenerator[Dict[str, Any], None]:
|
307
|
-
current_agent = agent; history = list(messages); context_vars = context_variables.copy() if context_variables else {}; logger.debug(f"Streaming run starting for '{current_agent.name}'. (Tool exec/handoff N/A)")
|
308
|
-
agent_tools = await self._get_agent_tools(current_agent); formatted_tools = format_tools_for_llm(agent_tools)
|
309
|
-
if debug and formatted_tools: logger.debug(f"Tools for '{current_agent.name}' (streaming): {[t['function']['name'] for t in formatted_tools]}")
|
310
|
-
try:
|
311
|
-
stream_generator = get_chat_completion_message(client=self.client, agent=current_agent, history=history, context_variables=context_vars, current_llm_config=self.current_llm_config, max_context_tokens=self.max_context_tokens, max_context_messages=self.max_context_messages, tools=formatted_tools or None, tool_choice="auto" if formatted_tools else None, stream=True, debug=debug)
|
312
|
-
async for chunk in stream_generator: yield chunk
|
313
|
-
logger.warning("Tool calls/handoffs not processed in streaming.")
|
314
|
-
except OpenAIError as e: logger.error(f"API error stream for '{current_agent.name}': {e}", exc_info=True); yield {"error": f"API call failed: {str(e)}"}
|
315
|
-
except Exception as e: logger.error(f"Error stream for '{current_agent.name}': {e}", exc_info=True); yield {"error": f"Unexpected error: {str(e)}"}
|
316
|
-
logger.debug(f"Streaming run finished for '{current_agent.name}'.")
|
317
|
-
|
318
|
-
async def run(self, agent: Agent, messages: List[Dict[str, Any]], context_variables: Optional[Dict[str, Any]] = None, max_turns: int = 10, stream: bool = False, debug: bool = False) -> Union[Response, AsyncGenerator[Dict[str, Any], None]]:
|
319
|
-
effective_debug = debug or self.debug
|
320
|
-
if effective_debug != logger.isEnabledFor(logging.DEBUG):
|
321
|
-
new_level = logging.DEBUG if effective_debug else settings.log_level.upper(); logger.setLevel(new_level); [h.setLevel(new_level) for h in logger.handlers]; logger.debug(f"Log level set to {new_level}.")
|
322
|
-
if not agent: raise ValueError("Agent cannot be None")
|
323
|
-
if not isinstance(messages, list): raise TypeError("Messages must be a list")
|
324
|
-
logger.info(f"Starting {'STREAMING' if stream else 'NON-STREAMING'} run with agent '{agent.name}'")
|
325
|
-
if stream: return self._run_streaming(agent, messages, context_variables, max_turns, effective_debug)
|
326
|
-
else: return await self._run_non_streaming(agent, messages, context_variables, max_turns, effective_debug)
|
swarm/extensions/mcp/__init__.py
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
# This is the __init__.py for the 'mcp' package.
|
@@ -1,36 +0,0 @@
|
|
1
|
-
# cache_utils.py
|
2
|
-
|
3
|
-
from typing import Any
|
4
|
-
|
5
|
-
class DummyCache:
|
6
|
-
"""A dummy cache that performs no operations."""
|
7
|
-
def get(self, key: str, default: Any = None) -> Any:
|
8
|
-
return default
|
9
|
-
|
10
|
-
def set(self, key: str, value: Any, timeout: int = None) -> None:
|
11
|
-
pass
|
12
|
-
|
13
|
-
def get_cache():
|
14
|
-
"""
|
15
|
-
Attempts to retrieve Django's cache. If Django isn't available or configured,
|
16
|
-
returns a DummyCache instance.
|
17
|
-
"""
|
18
|
-
try:
|
19
|
-
# Intentionally commented out Django imports to avoid mandatory dependency for now
|
20
|
-
# import django
|
21
|
-
# from django.conf import settings
|
22
|
-
# from django.core.cache import cache as django_cache
|
23
|
-
# from django.core.exceptions import ImproperlyConfigured
|
24
|
-
|
25
|
-
# if not settings.configured:
|
26
|
-
# # Django settings are not configured; return DummyCache
|
27
|
-
# return DummyCache()
|
28
|
-
|
29
|
-
# return django_cache
|
30
|
-
# For now, always return DummyCache until Django integration is confirmed/required
|
31
|
-
return DummyCache()
|
32
|
-
|
33
|
-
|
34
|
-
except (ImportError): # Removed ImproperlyConfigured as Django is not imported
|
35
|
-
# Django is not installed or not properly configured; use DummyCache
|
36
|
-
return DummyCache()
|