open-swarm 0.1.1745275181__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.1748636295.dist-info/RECORD +89 -0
- {open_swarm-0.1.1745275181.dist-info → open_swarm-0.1.1748636295.dist-info}/WHEEL +2 -1
- open_swarm-0.1.1748636295.dist-info/entry_points.txt +3 -0
- open_swarm-0.1.1748636295.dist-info/top_level.txt +1 -0
- swarm/__init__.py +2 -0
- swarm/agent/agent.py +49 -0
- swarm/auth.py +48 -113
- swarm/consumers.py +0 -19
- swarm/core.py +411 -0
- swarm/extensions/blueprint/__init__.py +16 -30
- swarm/extensions/blueprint/agent_utils.py +45 -0
- swarm/extensions/blueprint/blueprint_base.py +562 -0
- swarm/extensions/blueprint/blueprint_discovery.py +112 -0
- swarm/extensions/blueprint/django_utils.py +79 -181
- swarm/extensions/blueprint/interactive_mode.py +72 -67
- swarm/extensions/blueprint/output_utils.py +82 -0
- swarm/{core → extensions/blueprint}/spinner.py +21 -30
- swarm/extensions/cli/cli_args.py +0 -6
- swarm/extensions/cli/commands/blueprint_management.py +9 -47
- swarm/extensions/cli/commands/config_management.py +6 -5
- swarm/extensions/cli/commands/edit_config.py +7 -16
- swarm/extensions/cli/commands/list_blueprints.py +1 -1
- swarm/extensions/cli/commands/validate_env.py +4 -11
- swarm/extensions/cli/commands/validate_envvars.py +6 -6
- swarm/extensions/cli/interactive_shell.py +2 -16
- swarm/extensions/config/config_loader.py +345 -107
- swarm/{core → extensions/config}/config_manager.py +38 -50
- swarm/{core → extensions/config}/server_config.py +0 -32
- swarm/extensions/launchers/build_launchers.py +14 -0
- swarm/{core → extensions/launchers}/build_swarm_wrapper.py +0 -0
- swarm/extensions/launchers/swarm_api.py +64 -8
- swarm/extensions/launchers/swarm_cli.py +300 -8
- 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 +166 -0
- swarm/serializers.py +5 -96
- swarm/settings.py +133 -85
- swarm/types.py +91 -0
- swarm/urls.py +74 -57
- swarm/utils/context_utils.py +4 -10
- swarm/utils/general_utils.py +0 -21
- swarm/utils/redact.py +36 -23
- swarm/views/api_views.py +39 -48
- swarm/views/chat_views.py +76 -236
- swarm/views/core_views.py +87 -80
- swarm/views/model_views.py +121 -64
- swarm/views/utils.py +439 -65
- swarm/views/web_views.py +2 -2
- open_swarm-0.1.1745275181.dist-info/METADATA +0 -874
- open_swarm-0.1.1745275181.dist-info/RECORD +0 -319
- open_swarm-0.1.1745275181.dist-info/entry_points.txt +0 -4
- swarm/blueprints/README.md +0 -68
- swarm/blueprints/blueprint_audit_status.json +0 -27
- swarm/blueprints/chatbot/README.md +0 -40
- swarm/blueprints/chatbot/blueprint_chatbot.py +0 -471
- swarm/blueprints/chatbot/metadata.json +0 -23
- swarm/blueprints/chatbot/templates/chatbot/chatbot.html +0 -33
- swarm/blueprints/chucks_angels/README.md +0 -11
- swarm/blueprints/chucks_angels/blueprint_chucks_angels.py +0 -7
- swarm/blueprints/chucks_angels/test_basic.py +0 -3
- swarm/blueprints/codey/CODEY.md +0 -15
- swarm/blueprints/codey/README.md +0 -115
- swarm/blueprints/codey/blueprint_codey.py +0 -1072
- swarm/blueprints/codey/codey_cli.py +0 -373
- swarm/blueprints/codey/instructions.md +0 -17
- swarm/blueprints/codey/metadata.json +0 -23
- swarm/blueprints/common/operation_box_utils.py +0 -83
- swarm/blueprints/digitalbutlers/README.md +0 -11
- swarm/blueprints/digitalbutlers/__init__.py +0 -1
- swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py +0 -7
- swarm/blueprints/digitalbutlers/test_basic.py +0 -3
- swarm/blueprints/divine_code/README.md +0 -3
- swarm/blueprints/divine_code/__init__.py +0 -10
- swarm/blueprints/divine_code/apps.py +0 -11
- swarm/blueprints/divine_code/blueprint_divine_code.py +0 -270
- swarm/blueprints/django_chat/apps.py +0 -6
- swarm/blueprints/django_chat/blueprint_django_chat.py +0 -268
- swarm/blueprints/django_chat/templates/django_chat/django_chat_webpage.html +0 -37
- swarm/blueprints/django_chat/urls.py +0 -8
- swarm/blueprints/django_chat/views.py +0 -32
- swarm/blueprints/echocraft/blueprint_echocraft.py +0 -384
- swarm/blueprints/flock/README.md +0 -11
- swarm/blueprints/flock/__init__.py +0 -8
- swarm/blueprints/flock/blueprint_flock.py +0 -7
- swarm/blueprints/flock/test_basic.py +0 -3
- swarm/blueprints/geese/README.md +0 -10
- swarm/blueprints/geese/__init__.py +0 -8
- swarm/blueprints/geese/blueprint_geese.py +0 -384
- swarm/blueprints/geese/geese_cli.py +0 -102
- swarm/blueprints/jeeves/README.md +0 -41
- swarm/blueprints/jeeves/blueprint_jeeves.py +0 -722
- swarm/blueprints/jeeves/jeeves_cli.py +0 -55
- swarm/blueprints/jeeves/metadata.json +0 -24
- swarm/blueprints/mcp_demo/blueprint_mcp_demo.py +0 -473
- swarm/blueprints/messenger/templates/messenger/messenger.html +0 -46
- swarm/blueprints/mission_improbable/blueprint_mission_improbable.py +0 -423
- swarm/blueprints/monkai_magic/blueprint_monkai_magic.py +0 -340
- swarm/blueprints/nebula_shellz/blueprint_nebula_shellz.py +0 -265
- swarm/blueprints/omniplex/blueprint_omniplex.py +0 -298
- swarm/blueprints/poets/blueprint_poets.py +0 -546
- swarm/blueprints/poets/poets_cli.py +0 -23
- swarm/blueprints/rue_code/README.md +0 -8
- swarm/blueprints/rue_code/blueprint_rue_code.py +0 -448
- swarm/blueprints/rue_code/rue_code_cli.py +0 -43
- swarm/blueprints/stewie/apps.py +0 -12
- swarm/blueprints/stewie/blueprint_family_ties.py +0 -349
- swarm/blueprints/stewie/models.py +0 -19
- swarm/blueprints/stewie/serializers.py +0 -10
- swarm/blueprints/stewie/settings.py +0 -17
- swarm/blueprints/stewie/urls.py +0 -11
- swarm/blueprints/stewie/views.py +0 -26
- swarm/blueprints/suggestion/blueprint_suggestion.py +0 -222
- swarm/blueprints/whinge_surf/README.md +0 -22
- swarm/blueprints/whinge_surf/__init__.py +0 -1
- swarm/blueprints/whinge_surf/blueprint_whinge_surf.py +0 -565
- swarm/blueprints/whinge_surf/whinge_surf_cli.py +0 -99
- swarm/blueprints/whiskeytango_foxtrot/__init__.py +0 -0
- swarm/blueprints/whiskeytango_foxtrot/apps.py +0 -11
- swarm/blueprints/whiskeytango_foxtrot/blueprint_whiskeytango_foxtrot.py +0 -339
- swarm/blueprints/zeus/__init__.py +0 -2
- swarm/blueprints/zeus/apps.py +0 -4
- swarm/blueprints/zeus/blueprint_zeus.py +0 -270
- swarm/blueprints/zeus/zeus_cli.py +0 -13
- swarm/cli/async_input.py +0 -65
- swarm/cli/async_input_demo.py +0 -32
- swarm/core/agent_utils.py +0 -21
- swarm/core/blueprint_base.py +0 -769
- swarm/core/blueprint_discovery.py +0 -125
- swarm/core/blueprint_runner.py +0 -59
- swarm/core/blueprint_ux.py +0 -109
- swarm/core/build_launchers.py +0 -15
- swarm/core/cli/__init__.py +0 -1
- swarm/core/cli/commands/__init__.py +0 -1
- swarm/core/cli/commands/blueprint_management.py +0 -7
- swarm/core/cli/interactive_shell.py +0 -14
- swarm/core/cli/main.py +0 -50
- swarm/core/cli/utils/__init__.py +0 -1
- swarm/core/cli/utils/discover_commands.py +0 -18
- swarm/core/config_loader.py +0 -122
- swarm/core/output_utils.py +0 -193
- swarm/core/session_logger.py +0 -42
- swarm/core/slash_commands.py +0 -89
- swarm/core/swarm_api.py +0 -68
- swarm/core/swarm_cli.py +0 -216
- swarm/core/utils/__init__.py +0 -0
- swarm/extensions/blueprint/cli_handler.py +0 -197
- swarm/extensions/blueprint/runnable_blueprint.py +0 -42
- swarm/extensions/cli/utils/__init__.py +0 -1
- swarm/extensions/cli/utils/async_input.py +0 -46
- swarm/extensions/cli/utils/prompt_user.py +0 -3
- swarm/management/__init__.py +0 -0
- swarm/management/commands/__init__.py +0 -0
- swarm/management/commands/runserver.py +0 -58
- swarm/middleware.py +0 -65
- swarm/permissions.py +0 -38
- swarm/static/contrib/fonts/fontawesome-webfont.ttf +0 -7
- swarm/static/contrib/fonts/fontawesome-webfont.woff +0 -7
- swarm/static/contrib/fonts/fontawesome-webfont.woff2 +0 -7
- swarm/static/contrib/markedjs/marked.min.js +0 -6
- swarm/static/contrib/tabler-icons/adjustments-horizontal.svg +0 -27
- swarm/static/contrib/tabler-icons/alert-triangle.svg +0 -21
- swarm/static/contrib/tabler-icons/archive.svg +0 -21
- swarm/static/contrib/tabler-icons/artboard.svg +0 -27
- swarm/static/contrib/tabler-icons/automatic-gearbox.svg +0 -23
- swarm/static/contrib/tabler-icons/box-multiple.svg +0 -19
- swarm/static/contrib/tabler-icons/carambola.svg +0 -19
- swarm/static/contrib/tabler-icons/copy.svg +0 -20
- swarm/static/contrib/tabler-icons/download.svg +0 -21
- swarm/static/contrib/tabler-icons/edit.svg +0 -21
- swarm/static/contrib/tabler-icons/filled/carambola.svg +0 -13
- swarm/static/contrib/tabler-icons/filled/paint.svg +0 -13
- swarm/static/contrib/tabler-icons/headset.svg +0 -22
- swarm/static/contrib/tabler-icons/layout-sidebar-left-collapse.svg +0 -21
- swarm/static/contrib/tabler-icons/layout-sidebar-left-expand.svg +0 -21
- swarm/static/contrib/tabler-icons/layout-sidebar-right-collapse.svg +0 -21
- swarm/static/contrib/tabler-icons/layout-sidebar-right-expand.svg +0 -21
- swarm/static/contrib/tabler-icons/message-chatbot.svg +0 -22
- swarm/static/contrib/tabler-icons/message-star.svg +0 -22
- swarm/static/contrib/tabler-icons/message-x.svg +0 -23
- swarm/static/contrib/tabler-icons/message.svg +0 -21
- swarm/static/contrib/tabler-icons/paperclip.svg +0 -18
- swarm/static/contrib/tabler-icons/playlist-add.svg +0 -22
- swarm/static/contrib/tabler-icons/robot.svg +0 -26
- swarm/static/contrib/tabler-icons/search.svg +0 -19
- swarm/static/contrib/tabler-icons/settings.svg +0 -20
- swarm/static/contrib/tabler-icons/thumb-down.svg +0 -19
- swarm/static/contrib/tabler-icons/thumb-up.svg +0 -19
- swarm/static/css/dropdown.css +0 -22
- swarm/static/htmx/htmx.min.js +0 -0
- swarm/static/js/dropdown.js +0 -23
- swarm/static/rest_mode/css/base.css +0 -470
- swarm/static/rest_mode/css/chat-history.css +0 -286
- swarm/static/rest_mode/css/chat.css +0 -251
- swarm/static/rest_mode/css/chatbot.css +0 -74
- swarm/static/rest_mode/css/chatgpt.css +0 -62
- swarm/static/rest_mode/css/colors/corporate.css +0 -74
- swarm/static/rest_mode/css/colors/pastel.css +0 -81
- swarm/static/rest_mode/css/colors/tropical.css +0 -82
- swarm/static/rest_mode/css/general.css +0 -142
- swarm/static/rest_mode/css/layout.css +0 -167
- swarm/static/rest_mode/css/layouts/messenger-layout.css +0 -17
- swarm/static/rest_mode/css/layouts/minimalist-layout.css +0 -57
- swarm/static/rest_mode/css/layouts/mobile-layout.css +0 -8
- swarm/static/rest_mode/css/messages.css +0 -84
- swarm/static/rest_mode/css/messenger.css +0 -135
- swarm/static/rest_mode/css/settings.css +0 -91
- swarm/static/rest_mode/css/simple.css +0 -44
- swarm/static/rest_mode/css/slack.css +0 -58
- swarm/static/rest_mode/css/style.css +0 -156
- swarm/static/rest_mode/css/theme.css +0 -30
- swarm/static/rest_mode/css/toast.css +0 -40
- swarm/static/rest_mode/js/auth.js +0 -9
- swarm/static/rest_mode/js/blueprint.js +0 -41
- swarm/static/rest_mode/js/blueprintUtils.js +0 -12
- swarm/static/rest_mode/js/chatLogic.js +0 -79
- swarm/static/rest_mode/js/debug.js +0 -63
- swarm/static/rest_mode/js/events.js +0 -98
- swarm/static/rest_mode/js/main.js +0 -19
- swarm/static/rest_mode/js/messages.js +0 -264
- swarm/static/rest_mode/js/messengerLogic.js +0 -355
- swarm/static/rest_mode/js/modules/apiService.js +0 -84
- swarm/static/rest_mode/js/modules/blueprintManager.js +0 -162
- swarm/static/rest_mode/js/modules/chatHistory.js +0 -110
- swarm/static/rest_mode/js/modules/debugLogger.js +0 -14
- swarm/static/rest_mode/js/modules/eventHandlers.js +0 -107
- swarm/static/rest_mode/js/modules/messageProcessor.js +0 -120
- swarm/static/rest_mode/js/modules/state.js +0 -7
- swarm/static/rest_mode/js/modules/userInteractions.js +0 -29
- swarm/static/rest_mode/js/modules/validation.js +0 -23
- swarm/static/rest_mode/js/rendering.js +0 -119
- swarm/static/rest_mode/js/settings.js +0 -130
- swarm/static/rest_mode/js/sidebar.js +0 -94
- swarm/static/rest_mode/js/simpleLogic.js +0 -37
- swarm/static/rest_mode/js/slackLogic.js +0 -66
- swarm/static/rest_mode/js/splash.js +0 -76
- swarm/static/rest_mode/js/theme.js +0 -111
- swarm/static/rest_mode/js/toast.js +0 -36
- swarm/static/rest_mode/js/ui.js +0 -265
- swarm/static/rest_mode/js/validation.js +0 -57
- swarm/static/rest_mode/svg/animated_spinner.svg +0 -12
- swarm/static/rest_mode/svg/arrow_down.svg +0 -5
- swarm/static/rest_mode/svg/arrow_left.svg +0 -5
- swarm/static/rest_mode/svg/arrow_right.svg +0 -5
- swarm/static/rest_mode/svg/arrow_up.svg +0 -5
- swarm/static/rest_mode/svg/attach.svg +0 -8
- swarm/static/rest_mode/svg/avatar.svg +0 -7
- swarm/static/rest_mode/svg/canvas.svg +0 -6
- swarm/static/rest_mode/svg/chat_history.svg +0 -4
- swarm/static/rest_mode/svg/close.svg +0 -5
- swarm/static/rest_mode/svg/copy.svg +0 -4
- swarm/static/rest_mode/svg/dark_mode.svg +0 -3
- swarm/static/rest_mode/svg/edit.svg +0 -5
- swarm/static/rest_mode/svg/layout.svg +0 -9
- swarm/static/rest_mode/svg/logo.svg +0 -29
- swarm/static/rest_mode/svg/logout.svg +0 -5
- swarm/static/rest_mode/svg/mobile.svg +0 -5
- swarm/static/rest_mode/svg/new_chat.svg +0 -4
- swarm/static/rest_mode/svg/not_visible.svg +0 -5
- swarm/static/rest_mode/svg/plus.svg +0 -7
- swarm/static/rest_mode/svg/run_code.svg +0 -6
- swarm/static/rest_mode/svg/save.svg +0 -4
- swarm/static/rest_mode/svg/search.svg +0 -6
- swarm/static/rest_mode/svg/settings.svg +0 -4
- swarm/static/rest_mode/svg/speaker.svg +0 -5
- swarm/static/rest_mode/svg/stop.svg +0 -6
- swarm/static/rest_mode/svg/thumbs_down.svg +0 -3
- swarm/static/rest_mode/svg/thumbs_up.svg +0 -3
- swarm/static/rest_mode/svg/toggle_off.svg +0 -6
- swarm/static/rest_mode/svg/toggle_on.svg +0 -6
- swarm/static/rest_mode/svg/trash.svg +0 -10
- swarm/static/rest_mode/svg/undo.svg +0 -3
- swarm/static/rest_mode/svg/visible.svg +0 -8
- swarm/static/rest_mode/svg/voice.svg +0 -10
- swarm/templates/account/login.html +0 -22
- swarm/templates/account/signup.html +0 -32
- swarm/templates/base.html +0 -30
- swarm/templates/chat.html +0 -43
- swarm/templates/index.html +0 -35
- swarm/templates/rest_mode/components/chat_sidebar.html +0 -55
- swarm/templates/rest_mode/components/header.html +0 -45
- swarm/templates/rest_mode/components/main_chat_pane.html +0 -41
- swarm/templates/rest_mode/components/settings_dialog.html +0 -97
- swarm/templates/rest_mode/components/splash_screen.html +0 -7
- swarm/templates/rest_mode/components/top_bar.html +0 -28
- swarm/templates/rest_mode/message_ui.html +0 -50
- swarm/templates/rest_mode/slackbot.html +0 -30
- swarm/templates/simple_blueprint_page.html +0 -24
- swarm/templates/websocket_partials/final_system_message.html +0 -3
- swarm/templates/websocket_partials/system_message.html +0 -4
- swarm/templates/websocket_partials/user_message.html +0 -5
- swarm/utils/ansi_box.py +0 -34
- swarm/utils/disable_tracing.py +0 -38
- swarm/utils/log_utils.py +0 -63
- swarm/utils/openai_patch.py +0 -33
- swarm/ux/ansi_box.py +0 -43
- swarm/ux/spinner.py +0 -53
- {open_swarm-0.1.1745275181.dist-info → open_swarm-0.1.1748636295.dist-info}/licenses/LICENSE +0 -0
- /swarm/{core → extensions/blueprint}/blueprint_utils.py +0 -0
- /swarm/{core → extensions/blueprint}/common_utils.py +0 -0
- /swarm/{core → extensions/config}/setup_wizard.py +0 -0
- /swarm/{blueprints/rue_code → extensions/config/utils}/__init__.py +0 -0
- /swarm/{core → extensions/config}/utils/logger.py +0 -0
- /swarm/{core → extensions/launchers}/swarm_wrapper.py +0 -0
@@ -0,0 +1,166 @@
|
|
1
|
+
"""
|
2
|
+
Chat Completion Module
|
3
|
+
|
4
|
+
This module handles chat completion logic for the Swarm framework, including message preparation,
|
5
|
+
tool call repair, and interaction with the OpenAI API. Located in llm/ for LLM-specific functionality.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
import json
|
10
|
+
import logging
|
11
|
+
from typing import List, Optional, Dict, Any
|
12
|
+
from collections import defaultdict
|
13
|
+
|
14
|
+
import asyncio
|
15
|
+
from openai import AsyncOpenAI, OpenAIError
|
16
|
+
from ..types import ChatCompletionMessage, Agent
|
17
|
+
from ..utils.redact import redact_sensitive_data
|
18
|
+
from ..utils.general_utils import serialize_datetime
|
19
|
+
from ..utils.message_utils import filter_duplicate_system_messages, update_null_content
|
20
|
+
from ..utils.context_utils import get_token_count, truncate_message_history
|
21
|
+
from ..utils.message_sequence import repair_message_payload
|
22
|
+
|
23
|
+
# Configure module-level logging
|
24
|
+
logger = logging.getLogger(__name__)
|
25
|
+
logger.setLevel(logging.DEBUG)
|
26
|
+
if not logger.handlers:
|
27
|
+
stream_handler = logging.StreamHandler()
|
28
|
+
formatter = logging.Formatter("[%(levelname)s] %(asctime)s - %(name)s - %(message)s")
|
29
|
+
stream_handler.setFormatter(formatter)
|
30
|
+
logger.addHandler(stream_handler)
|
31
|
+
|
32
|
+
|
33
|
+
async def get_chat_completion(
|
34
|
+
client: AsyncOpenAI,
|
35
|
+
agent: Agent,
|
36
|
+
history: List[Dict[str, Any]],
|
37
|
+
context_variables: dict,
|
38
|
+
current_llm_config: Dict[str, Any],
|
39
|
+
max_context_tokens: int,
|
40
|
+
max_context_messages: int,
|
41
|
+
model_override: Optional[str] = None,
|
42
|
+
stream: bool = False,
|
43
|
+
debug: bool = False
|
44
|
+
) -> ChatCompletionMessage:
|
45
|
+
"""
|
46
|
+
Retrieve a chat completion from the LLM for the given agent and history.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
client: AsyncOpenAI client instance.
|
50
|
+
agent: The agent processing the completion.
|
51
|
+
history: List of previous messages in the conversation.
|
52
|
+
context_variables: Variables to include in the agent's context.
|
53
|
+
current_llm_config: Current LLM configuration dictionary.
|
54
|
+
max_context_tokens: Maximum token limit for context.
|
55
|
+
max_context_messages: Maximum message limit for context.
|
56
|
+
model_override: Optional model to use instead of default.
|
57
|
+
stream: If True, stream the response; otherwise, return complete.
|
58
|
+
debug: If True, log detailed debugging information.
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
ChatCompletionMessage: The LLM's response message.
|
62
|
+
"""
|
63
|
+
if not agent:
|
64
|
+
logger.error("Cannot generate chat completion: Agent is None")
|
65
|
+
raise ValueError("Agent is required")
|
66
|
+
|
67
|
+
logger.debug(f"Generating chat completion for agent '{agent.name}'")
|
68
|
+
active_model = model_override or current_llm_config.get("model", "default")
|
69
|
+
client_kwargs = {
|
70
|
+
"api_key": current_llm_config.get("api_key"),
|
71
|
+
"base_url": current_llm_config.get("base_url")
|
72
|
+
}
|
73
|
+
client_kwargs = {k: v for k, v in client_kwargs.items() if v is not None}
|
74
|
+
redacted_kwargs = redact_sensitive_data(client_kwargs, sensitive_keys=["api_key"])
|
75
|
+
logger.debug(f"Using client with model='{active_model}', base_url='{client_kwargs.get('base_url', 'default')}', api_key={redacted_kwargs['api_key']}")
|
76
|
+
|
77
|
+
context_variables = defaultdict(str, context_variables)
|
78
|
+
instructions = agent.instructions(context_variables) if callable(agent.instructions) else agent.instructions
|
79
|
+
if not isinstance(instructions, str):
|
80
|
+
logger.warning(f"Invalid instructions type for '{agent.name}': {type(instructions)}. Converting to string.")
|
81
|
+
instructions = str(instructions)
|
82
|
+
messages = repair_message_payload([{"role": "system", "content": instructions}], debug=debug)
|
83
|
+
|
84
|
+
if not isinstance(history, list):
|
85
|
+
logger.error(f"Invalid history type for '{agent.name}': {type(history)}. Expected list.")
|
86
|
+
history = []
|
87
|
+
seen_ids = set()
|
88
|
+
for msg in history:
|
89
|
+
msg_id = msg.get("id", hash(json.dumps(msg, sort_keys=True, default=serialize_datetime)))
|
90
|
+
if msg_id not in seen_ids:
|
91
|
+
seen_ids.add(msg_id)
|
92
|
+
if "tool_calls" in msg and msg["tool_calls"] is not None and not isinstance(msg["tool_calls"], list):
|
93
|
+
logger.warning(f"Invalid tool_calls in history for '{msg.get('sender', 'unknown')}': {msg['tool_calls']}. Setting to None.")
|
94
|
+
msg["tool_calls"] = None
|
95
|
+
messages.append(msg)
|
96
|
+
messages = filter_duplicate_system_messages(messages)
|
97
|
+
messages = truncate_message_history(messages, active_model, max_context_tokens, max_context_messages)
|
98
|
+
messages = repair_message_payload(messages, debug=debug) # Ensure tool calls are paired post-truncation
|
99
|
+
|
100
|
+
logger.debug(f"Prepared {len(messages)} messages for '{agent.name}'")
|
101
|
+
if debug:
|
102
|
+
logger.debug(f"Messages: {json.dumps(messages, indent=2, default=str)}")
|
103
|
+
|
104
|
+
create_params = {
|
105
|
+
"model": active_model,
|
106
|
+
"messages": messages,
|
107
|
+
"stream": stream,
|
108
|
+
"temperature": current_llm_config.get("temperature", 0.7),
|
109
|
+
}
|
110
|
+
if getattr(agent, "response_format", None):
|
111
|
+
create_params["response_format"] = agent.response_format
|
112
|
+
create_params = {k: v for k, v in create_params.items() if v is not None}
|
113
|
+
logger.debug(f"Chat completion params: model='{active_model}', messages_count={len(messages)}, stream={stream}")
|
114
|
+
|
115
|
+
try:
|
116
|
+
logger.debug(f"Calling OpenAI API for '{agent.name}' with model='{active_model}'")
|
117
|
+
prev_openai_api_key = os.environ.pop("OPENAI_API_KEY", None)
|
118
|
+
try:
|
119
|
+
completion = await client.chat.completions.create(**create_params)
|
120
|
+
if stream:
|
121
|
+
return completion # Return stream object directly
|
122
|
+
if completion.choices and len(completion.choices) > 0 and completion.choices[0].message:
|
123
|
+
log_msg = completion.choices[0].message.content[:50] if completion.choices[0].message.content else "No content"
|
124
|
+
logger.debug(f"OpenAI completion received for '{agent.name}': {log_msg}...")
|
125
|
+
return completion.choices[0].message
|
126
|
+
else:
|
127
|
+
logger.warning(f"No valid message in completion for '{agent.name}'")
|
128
|
+
return ChatCompletionMessage(content="No response generated", role="assistant")
|
129
|
+
finally:
|
130
|
+
if prev_openai_api_key is not None:
|
131
|
+
os.environ["OPENAI_API_KEY"] = prev_openai_api_key
|
132
|
+
except OpenAIError as e:
|
133
|
+
logger.error(f"Chat completion failed for '{agent.name}': {e}")
|
134
|
+
raise
|
135
|
+
|
136
|
+
|
137
|
+
async def get_chat_completion_message(
|
138
|
+
client: AsyncOpenAI,
|
139
|
+
agent: Agent,
|
140
|
+
history: List[Dict[str, Any]],
|
141
|
+
context_variables: dict,
|
142
|
+
current_llm_config: Dict[str, Any],
|
143
|
+
max_context_tokens: int,
|
144
|
+
max_context_messages: int,
|
145
|
+
model_override: Optional[str] = None,
|
146
|
+
stream: bool = False,
|
147
|
+
debug: bool = False
|
148
|
+
) -> ChatCompletionMessage:
|
149
|
+
"""
|
150
|
+
Wrapper to retrieve and validate a chat completion message.
|
151
|
+
|
152
|
+
Args:
|
153
|
+
Same as get_chat_completion.
|
154
|
+
|
155
|
+
Returns:
|
156
|
+
ChatCompletionMessage: Validated LLM response message.
|
157
|
+
"""
|
158
|
+
logger.debug(f"Fetching chat completion message for '{agent.name}'")
|
159
|
+
completion = await get_chat_completion(
|
160
|
+
client, agent, history, context_variables, current_llm_config,
|
161
|
+
max_context_tokens, max_context_messages, model_override, stream, debug
|
162
|
+
)
|
163
|
+
if isinstance(completion, ChatCompletionMessage):
|
164
|
+
return completion
|
165
|
+
logger.warning(f"Unexpected completion type: {type(completion)}. Converting to ChatCompletionMessage.")
|
166
|
+
return ChatCompletionMessage(content=str(completion), role="assistant")
|
swarm/serializers.py
CHANGED
@@ -1,103 +1,12 @@
|
|
1
1
|
from rest_framework import serializers
|
2
|
-
from swarm.models import ChatMessage
|
3
|
-
import logging
|
4
|
-
|
5
|
-
logger = logging.getLogger(__name__)
|
6
|
-
print_logger = logging.getLogger('print_debug')
|
7
|
-
|
8
|
-
class MessageSerializer(serializers.Serializer):
|
9
|
-
role = serializers.ChoiceField(choices=["system", "user", "assistant", "tool"])
|
10
|
-
# Content is CharField, allows null/blank by default
|
11
|
-
content = serializers.CharField(allow_null=True, required=False, allow_blank=True)
|
12
|
-
name = serializers.CharField(required=False, allow_blank=True)
|
13
|
-
|
14
|
-
# Removed validate_content
|
15
|
-
|
16
|
-
def validate(self, data):
|
17
|
-
"""Validate message structure based on role."""
|
18
|
-
print_logger.debug(f"MessageSerializer.validate received data: {data}")
|
19
|
-
role = data.get('role')
|
20
|
-
content = data.get('content', None)
|
21
|
-
name = data.get('name')
|
22
|
-
|
23
|
-
# Role validation
|
24
|
-
if 'role' not in data:
|
25
|
-
raise serializers.ValidationError({"role": ["This field is required."]})
|
26
|
-
|
27
|
-
# Content requiredness validation (based on role)
|
28
|
-
content_required = role in ['system', 'user', 'assistant', 'tool']
|
29
|
-
content_present = 'content' in data
|
30
|
-
|
31
|
-
if content_required:
|
32
|
-
if not content_present:
|
33
|
-
raise serializers.ValidationError({"content": ["This field is required."]})
|
34
|
-
# Null/Blank checks are handled by field definition (allow_null/allow_blank)
|
35
|
-
# Type check will happen in ChatCompletionRequestSerializer.validate_messages
|
36
|
-
|
37
|
-
# Name validation for tool role
|
38
|
-
if role == 'tool' and not name:
|
39
|
-
raise serializers.ValidationError({"name": ["This field is required for role 'tool'."]})
|
40
|
-
|
41
|
-
print_logger.debug(f"MessageSerializer.validate PASSED for data: {data}")
|
42
|
-
return data
|
43
|
-
|
44
|
-
class ChatCompletionRequestSerializer(serializers.Serializer):
|
45
|
-
model = serializers.CharField(max_length=255)
|
46
|
-
messages = MessageSerializer(many=True, min_length=1)
|
47
|
-
stream = serializers.BooleanField(default=False)
|
48
|
-
params = serializers.JSONField(required=False, allow_null=True)
|
49
|
-
|
50
|
-
def validate(self, data):
|
51
|
-
"""Perform object-level validation."""
|
52
|
-
model_value = self.initial_data.get('model')
|
53
|
-
logger.debug(f"Top-level validate checking model type. Got: {type(model_value)}, value: {model_value}")
|
54
|
-
if model_value is not None and not isinstance(model_value, str):
|
55
|
-
raise serializers.ValidationError({"model": ["Field 'model' must be a string."]})
|
56
|
-
# Messages validation (including content type) happens in validate_messages
|
57
|
-
return data
|
58
|
-
|
59
|
-
def validate_messages(self, value):
|
60
|
-
"""
|
61
|
-
Validate the messages list itself and perform raw type checks.
|
62
|
-
'value' here is the list *after* MessageSerializer has run on each item.
|
63
|
-
We need to inspect `self.initial_data` for the raw types.
|
64
|
-
"""
|
65
|
-
if not value:
|
66
|
-
raise serializers.ValidationError("Messages list cannot be empty.")
|
67
|
-
|
68
|
-
# Access raw message data from initial_data for type checking
|
69
|
-
raw_messages = self.initial_data.get('messages', [])
|
70
|
-
if not isinstance(raw_messages, list):
|
71
|
-
# This case is handled by ListField implicitly, but good to be explicit
|
72
|
-
raise serializers.ValidationError("Expected a list of message items.")
|
73
|
-
|
74
|
-
errors = []
|
75
|
-
for i, raw_msg in enumerate(raw_messages):
|
76
|
-
msg_errors = {}
|
77
|
-
if not isinstance(raw_msg, dict):
|
78
|
-
# If the item itself isn't a dict, add error and skip further checks for it
|
79
|
-
errors.append({f"item_{i}": "Each message must be a dictionary."})
|
80
|
-
continue
|
81
|
-
|
82
|
-
# *** Check raw content type here ***
|
83
|
-
content = raw_msg.get('content', None)
|
84
|
-
if 'content' in raw_msg and content is not None and not isinstance(content, str):
|
85
|
-
msg_errors['content'] = ["Content must be a string or null."] # Match test assertion
|
86
|
-
|
87
|
-
# Add other raw checks if needed (e.g., role type)
|
88
|
-
|
89
|
-
if msg_errors:
|
90
|
-
errors.append(msg_errors) # Append errors for this specific message index
|
91
|
-
|
92
|
-
if errors:
|
93
|
-
# Raise a single validation error containing all message-specific errors
|
94
|
-
raise serializers.ValidationError(errors)
|
95
|
-
|
96
|
-
# Return the processed 'value' which passed MessageSerializer validation
|
97
|
-
return value
|
2
|
+
from swarm.models import ChatMessage, ChatConversation
|
98
3
|
|
99
4
|
class ChatMessageSerializer(serializers.ModelSerializer):
|
100
5
|
class Meta:
|
101
6
|
model = ChatMessage
|
102
7
|
fields = '__all__'
|
103
8
|
|
9
|
+
class ChatConversationSerializer(serializers.ModelSerializer):
|
10
|
+
class Meta:
|
11
|
+
model = ChatConversation
|
12
|
+
fields = '__all__'
|
swarm/settings.py
CHANGED
@@ -3,44 +3,27 @@ Django settings for swarm project.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
import os
|
6
|
+
import sys
|
6
7
|
from pathlib import Path
|
7
|
-
from dotenv import load_dotenv
|
8
8
|
import logging
|
9
9
|
|
10
|
-
|
10
|
+
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
11
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
12
|
+
PROJECT_ROOT = BASE_DIR.parent
|
13
|
+
if str(PROJECT_ROOT) not in sys.path:
|
14
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
11
15
|
|
12
|
-
|
13
|
-
dotenv_path = BASE_DIR.parent / '.env'
|
14
|
-
load_dotenv(dotenv_path=dotenv_path)
|
15
|
-
# print(f"[Settings] Attempted to load .env from: {dotenv_path}")
|
16
|
-
# ---
|
16
|
+
BLUEPRINTS_DIR = PROJECT_ROOT / 'blueprints'
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')
|
18
|
+
# --- Determine if running under pytest ---
|
19
|
+
TESTING = 'pytest' in sys.modules
|
21
20
|
|
22
|
-
#
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
# *** Only enable API auth if the token is actually set ***
|
27
|
-
ENABLE_API_AUTH = bool(_raw_api_token)
|
28
|
-
SWARM_API_KEY = _raw_api_token # Assign the loaded token (or None)
|
29
|
-
|
30
|
-
if ENABLE_API_AUTH:
|
31
|
-
# Add assertion to satisfy type checkers within this block
|
32
|
-
assert SWARM_API_KEY is not None, "SWARM_API_KEY cannot be None when ENABLE_API_AUTH is True"
|
33
|
-
# print(f"[Settings] SWARM_API_KEY loaded: {SWARM_API_KEY[:4]}...{SWARM_API_KEY[-4:]}")
|
34
|
-
# print("[Settings] ENABLE_API_AUTH is True.")
|
35
|
-
else:
|
36
|
-
# print("[Settings] API_AUTH_TOKEN env var not set. SWARM_API_KEY is None.")
|
37
|
-
# print("[Settings] ENABLE_API_AUTH is False.")
|
38
|
-
pass
|
39
|
-
|
40
|
-
SWARM_CONFIG_PATH = os.getenv('SWARM_CONFIG_PATH', str(BASE_DIR.parent / 'swarm_config.json'))
|
41
|
-
BLUEPRINT_DIRECTORY = os.getenv('BLUEPRINT_DIRECTORY', str(BASE_DIR / 'swarm' / 'blueprints'))
|
42
|
-
# --- End Custom Swarm Settings ---
|
21
|
+
# Quick-start development settings - unsuitable for production
|
22
|
+
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'django-insecure-YOUR_FALLBACK_KEY_HERE_CHANGE_ME')
|
23
|
+
DEBUG = os.getenv('DEBUG', 'True') == 'True'
|
24
|
+
ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '*').split(',')
|
43
25
|
|
26
|
+
# --- Application definition ---
|
44
27
|
INSTALLED_APPS = [
|
45
28
|
'django.contrib.admin',
|
46
29
|
'django.contrib.auth',
|
@@ -48,20 +31,73 @@ INSTALLED_APPS = [
|
|
48
31
|
'django.contrib.sessions',
|
49
32
|
'django.contrib.messages',
|
50
33
|
'django.contrib.staticfiles',
|
34
|
+
# Third-party apps
|
51
35
|
'rest_framework',
|
52
36
|
'rest_framework.authtoken',
|
53
37
|
'drf_spectacular',
|
54
|
-
|
38
|
+
# Local apps
|
39
|
+
'swarm.apps.SwarmConfig',
|
55
40
|
]
|
56
41
|
|
42
|
+
# --- Conditionally add blueprint apps for TESTING ---
|
43
|
+
# This ensures the app is known *before* django.setup() is called by pytest-django
|
44
|
+
if TESTING:
|
45
|
+
# Add specific apps needed for testing
|
46
|
+
# We know 'university' is needed based on SWARM_BLUEPRINTS in conftest
|
47
|
+
_test_apps_to_add = ['blueprints.university'] # Hardcoding for University tests specifically
|
48
|
+
for app in _test_apps_to_add:
|
49
|
+
if app not in INSTALLED_APPS:
|
50
|
+
# Use insert for potentially better ordering if it matters, otherwise append is fine
|
51
|
+
INSTALLED_APPS.insert(0, app) # Or INSTALLED_APPS.append(app)
|
52
|
+
logging.info(f"Settings [TESTING]: Added '{app}' to INSTALLED_APPS.")
|
53
|
+
# Ensure SWARM_BLUEPRINTS is set if your conftest or other logic relies on it
|
54
|
+
# Note: Setting it here might be redundant if conftest sets it too.
|
55
|
+
if 'SWARM_BLUEPRINTS' not in os.environ:
|
56
|
+
os.environ['SWARM_BLUEPRINTS'] = 'university'
|
57
|
+
logging.info(f"Settings [TESTING]: Set SWARM_BLUEPRINTS='university'")
|
58
|
+
|
59
|
+
else:
|
60
|
+
# --- Dynamic App Loading for Production/Development ---
|
61
|
+
_INITIAL_BLUEPRINT_APPS = []
|
62
|
+
_swarm_blueprints_env = os.getenv('SWARM_BLUEPRINTS')
|
63
|
+
_log_source = "Not Set"
|
64
|
+
if _swarm_blueprints_env:
|
65
|
+
_blueprint_names = [name.strip() for name in _swarm_blueprints_env.split(',') if name.strip()]
|
66
|
+
_INITIAL_BLUEPRINT_APPS = [f'blueprints.{name}' for name in _blueprint_names if name.replace('_', '').isidentifier()]
|
67
|
+
_log_source = "SWARM_BLUEPRINTS env var"
|
68
|
+
logging.info(f"Settings: Found blueprints from env var: {_INITIAL_BLUEPRINT_APPS}")
|
69
|
+
else:
|
70
|
+
_log_source = "directory scan"
|
71
|
+
try:
|
72
|
+
if BLUEPRINTS_DIR.is_dir():
|
73
|
+
for item in BLUEPRINTS_DIR.iterdir():
|
74
|
+
if item.is_dir() and (item / '__init__.py').exists():
|
75
|
+
if item.name.replace('_', '').isidentifier():
|
76
|
+
_INITIAL_BLUEPRINT_APPS.append(f'blueprints.{item.name}')
|
77
|
+
logging.info(f"Settings: Found blueprints from directory scan: {_INITIAL_BLUEPRINT_APPS}")
|
78
|
+
except Exception as e:
|
79
|
+
logging.error(f"Settings: Error discovering blueprint apps during initial load: {e}")
|
80
|
+
|
81
|
+
# Add dynamically discovered apps for non-testing scenarios
|
82
|
+
for app in _INITIAL_BLUEPRINT_APPS:
|
83
|
+
if app not in INSTALLED_APPS:
|
84
|
+
INSTALLED_APPS.append(app)
|
85
|
+
logging.info(f"Settings [{_log_source}]: Added '{app}' to INSTALLED_APPS.")
|
86
|
+
# --- End App Loading Logic ---
|
87
|
+
|
88
|
+
# Ensure INSTALLED_APPS is a list for compatibility
|
89
|
+
if isinstance(INSTALLED_APPS, tuple):
|
90
|
+
INSTALLED_APPS = list(INSTALLED_APPS)
|
91
|
+
|
92
|
+
logging.info(f"Settings: Final INSTALLED_APPS = {INSTALLED_APPS}")
|
93
|
+
|
94
|
+
|
57
95
|
MIDDLEWARE = [
|
58
96
|
'django.middleware.security.SecurityMiddleware',
|
59
97
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
60
98
|
'django.middleware.common.CommonMiddleware',
|
61
99
|
'django.middleware.csrf.CsrfViewMiddleware',
|
62
100
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
63
|
-
# Add custom middleware to handle async user loading after standard auth
|
64
|
-
'swarm.middleware.AsyncAuthMiddleware',
|
65
101
|
'django.contrib.messages.middleware.MessageMiddleware',
|
66
102
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
67
103
|
]
|
@@ -71,7 +107,7 @@ ROOT_URLCONF = 'swarm.urls'
|
|
71
107
|
TEMPLATES = [
|
72
108
|
{
|
73
109
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
74
|
-
'DIRS': [BASE_DIR
|
110
|
+
'DIRS': [BASE_DIR / 'templates'],
|
75
111
|
'APP_DIRS': True,
|
76
112
|
'OPTIONS': {
|
77
113
|
'context_processors': [
|
@@ -85,93 +121,105 @@ TEMPLATES = [
|
|
85
121
|
]
|
86
122
|
|
87
123
|
WSGI_APPLICATION = 'swarm.wsgi.application'
|
88
|
-
ASGI_APPLICATION = 'swarm.asgi.application'
|
89
124
|
|
125
|
+
# Database
|
126
|
+
SQLITE_DB_PATH = os.getenv('SQLITE_DB_PATH', BASE_DIR / 'db.sqlite3')
|
90
127
|
DATABASES = {
|
91
128
|
'default': {
|
92
129
|
'ENGINE': 'django.db.backends.sqlite3',
|
93
|
-
'NAME':
|
94
|
-
'TEST': {
|
95
|
-
'NAME': os.environ.get('DJANGO_TEST_DB_NAME', '/tmp/test_db.sqlite3'),
|
96
|
-
'OPTIONS': {
|
97
|
-
'timeout': 20,
|
98
|
-
'init_command': "PRAGMA journal_mode=WAL;",
|
99
|
-
},
|
100
|
-
},
|
130
|
+
'NAME': SQLITE_DB_PATH,
|
101
131
|
}
|
102
132
|
}
|
133
|
+
DJANGO_DATABASE = DATABASES['default']
|
103
134
|
|
135
|
+
|
136
|
+
# Password validation
|
104
137
|
AUTH_PASSWORD_VALIDATORS = [
|
105
|
-
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'
|
106
|
-
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'
|
107
|
-
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'
|
108
|
-
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'
|
138
|
+
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
|
139
|
+
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
|
140
|
+
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
|
141
|
+
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
|
109
142
|
]
|
110
143
|
|
144
|
+
|
145
|
+
# Internationalization
|
111
146
|
LANGUAGE_CODE = 'en-us'
|
112
147
|
TIME_ZONE = 'UTC'
|
113
148
|
USE_I18N = True
|
114
149
|
USE_TZ = True
|
115
150
|
|
116
|
-
STATIC_URL = 'static/'
|
117
|
-
STATIC_ROOT = BASE_DIR.parent / 'staticfiles'
|
118
|
-
STATICFILES_DIRS = [ BASE_DIR / "swarm" / "static", ]
|
119
151
|
|
152
|
+
# Static files
|
153
|
+
STATIC_URL = '/static/'
|
154
|
+
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
155
|
+
STATICFILES_DIRS = [
|
156
|
+
BASE_DIR / 'static',
|
157
|
+
BASE_DIR / 'assets',
|
158
|
+
]
|
159
|
+
|
160
|
+
# Default primary key field type
|
120
161
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
121
162
|
|
163
|
+
# REST Framework settings
|
122
164
|
REST_FRAMEWORK = {
|
123
|
-
'DEFAULT_AUTHENTICATION_CLASSES': [
|
124
|
-
'swarm.auth.StaticTokenAuthentication',
|
125
|
-
'swarm.auth.CustomSessionAuthentication',
|
126
|
-
],
|
127
|
-
# *** IMPORTANT: Add DEFAULT_PERMISSION_CLASSES ***
|
128
|
-
# If ENABLE_API_AUTH is False, we might want to allow any access for testing.
|
129
|
-
# If ENABLE_API_AUTH is True, we require HasValidTokenOrSession.
|
130
|
-
# We need to set this dynamically based on ENABLE_API_AUTH.
|
131
|
-
# A simple way is to set it here, but a cleaner way might involve middleware
|
132
|
-
# or overriding get_permissions in views. For now, let's adjust this:
|
133
|
-
'DEFAULT_PERMISSION_CLASSES': [
|
134
|
-
# If auth is enabled, require our custom permission
|
135
|
-
'swarm.permissions.HasValidTokenOrSession' if ENABLE_API_AUTH else
|
136
|
-
# Otherwise, allow anyone (useful for dev when token isn't set)
|
137
|
-
'rest_framework.permissions.AllowAny'
|
138
|
-
],
|
139
165
|
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
166
|
+
'DEFAULT_AUTHENTICATION_CLASSES': (
|
167
|
+
'swarm.auth.EnvOrTokenAuthentication',
|
168
|
+
'rest_framework.authentication.TokenAuthentication',
|
169
|
+
'rest_framework.authentication.SessionAuthentication',
|
170
|
+
),
|
171
|
+
'DEFAULT_PERMISSION_CLASSES': (
|
172
|
+
'rest_framework.permissions.IsAuthenticated',
|
173
|
+
)
|
140
174
|
}
|
141
175
|
|
142
176
|
SPECTACULAR_SETTINGS = {
|
143
177
|
'TITLE': 'Open Swarm API',
|
144
|
-
'DESCRIPTION': 'API for
|
145
|
-
'VERSION': '0.
|
178
|
+
'DESCRIPTION': 'API for the Open Swarm multi-agent collaboration framework.',
|
179
|
+
'VERSION': '1.0.0',
|
146
180
|
'SERVE_INCLUDE_SCHEMA': False,
|
181
|
+
'SERVE_PERMISSIONS': ['rest_framework.permissions.AllowAny'],
|
147
182
|
}
|
148
183
|
|
184
|
+
# Logging configuration
|
149
185
|
LOGGING = {
|
150
186
|
'version': 1,
|
151
187
|
'disable_existing_loggers': False,
|
152
188
|
'formatters': {
|
153
|
-
|
154
|
-
|
189
|
+
'standard': {
|
190
|
+
'format': '[%(levelname)s] %(asctime)s - %(name)s:%(lineno)d - %(message)s'
|
191
|
+
},
|
155
192
|
},
|
156
193
|
'handlers': {
|
157
|
-
'console': {
|
194
|
+
'console': {
|
195
|
+
'level': 'DEBUG' if DEBUG else 'INFO',
|
196
|
+
'class': 'logging.StreamHandler',
|
197
|
+
'formatter': 'standard',
|
198
|
+
},
|
158
199
|
},
|
159
200
|
'loggers': {
|
160
|
-
'django': { 'handlers': ['console'], 'level':
|
161
|
-
'
|
162
|
-
'swarm
|
163
|
-
'swarm.
|
164
|
-
'
|
165
|
-
'blueprint_django_chat': { 'handlers': ['console'], 'level': 'DEBUG', 'propagate': False, },
|
166
|
-
'print_debug': { 'handlers': ['console'], 'level': 'DEBUG', 'propagate': False, },
|
201
|
+
'django': { 'handlers': ['console'], 'level': 'INFO', 'propagate': False, },
|
202
|
+
'django.request': { 'handlers': ['console'], 'level': 'WARNING', 'propagate': False, },
|
203
|
+
'swarm': { 'handlers': ['console'], 'level': 'DEBUG' if DEBUG else 'INFO', 'propagate': False, },
|
204
|
+
'swarm.extensions': { 'handlers': ['console'], 'level': 'DEBUG' if DEBUG else 'INFO', 'propagate': False, },
|
205
|
+
'blueprints': { 'handlers': ['console'], 'level': 'DEBUG' if DEBUG else 'INFO', 'propagate': False, },
|
167
206
|
},
|
168
|
-
'root': { 'handlers': ['console'], 'level': 'WARNING', },
|
169
207
|
}
|
170
208
|
|
209
|
+
# Authentication backends
|
210
|
+
AUTHENTICATION_BACKENDS = [
|
211
|
+
'django.contrib.auth.backends.ModelBackend',
|
212
|
+
]
|
213
|
+
|
214
|
+
# Login URL
|
215
|
+
LOGIN_URL = '/accounts/login/'
|
216
|
+
LOGIN_REDIRECT_URL = '/chatbot/'
|
217
|
+
|
218
|
+
# Redis settings
|
171
219
|
REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
|
172
|
-
REDIS_PORT = int(os.getenv('REDIS_PORT',
|
220
|
+
REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))
|
173
221
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
222
|
+
# Adjust DB for testing if TESTING flag is set
|
223
|
+
if TESTING:
|
224
|
+
print("Pytest detected: Adjusting settings for testing.")
|
225
|
+
DATABASES['default']['NAME'] = ':memory:'
|
swarm/types.py
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
from openai.types.chat import ChatCompletionMessage
|
2
|
+
from openai.types.chat.chat_completion_message_tool_call import (
|
3
|
+
ChatCompletionMessageToolCall,
|
4
|
+
Function,
|
5
|
+
)
|
6
|
+
from typing import List, Callable, Union, Optional, Dict, Any
|
7
|
+
|
8
|
+
from pydantic import BaseModel, ConfigDict
|
9
|
+
|
10
|
+
# AgentFunction = Callable[[], Union[str, "Agent", dict]]
|
11
|
+
AgentFunction = Callable[..., Union[str, "Agent", dict]]
|
12
|
+
|
13
|
+
class Agent(BaseModel):
|
14
|
+
# model_config = ConfigDict(arbitrary_types_allowed=True) # Allow non-Pydantic types (for nemo guardrails instance)
|
15
|
+
|
16
|
+
name: str = "Agent"
|
17
|
+
model: str = "default"
|
18
|
+
instructions: Union[str, Callable[[], str]] = "You are a helpful agent."
|
19
|
+
functions: List[AgentFunction] = []
|
20
|
+
resources: List[Dict[str, Any]] = [] # New attribute for static and MCP-discovered resources
|
21
|
+
tool_choice: str = None
|
22
|
+
# parallel_tool_calls: bool = True # Commented out as in your version
|
23
|
+
parallel_tool_calls: bool = False
|
24
|
+
mcp_servers: Optional[List[str]] = None # List of MCP server names
|
25
|
+
env_vars: Optional[Dict[str, str]] = None # Environment variables required
|
26
|
+
response_format: Optional[Dict[str, Any]] = None # Structured Output
|
27
|
+
|
28
|
+
class Response(BaseModel):
|
29
|
+
id: Optional[str] = None # id needed for REST
|
30
|
+
messages: List = [] # Adjusted to allow any list (flexible for messages)
|
31
|
+
agent: Optional[Agent] = None
|
32
|
+
context_variables: dict = {}
|
33
|
+
|
34
|
+
def __init__(self, **kwargs):
|
35
|
+
super().__init__(**kwargs)
|
36
|
+
# Automatically generate an ID if not provided
|
37
|
+
if not self.id:
|
38
|
+
import uuid
|
39
|
+
self.id = f"response-{uuid.uuid4()}"
|
40
|
+
|
41
|
+
class Result(BaseModel):
|
42
|
+
"""
|
43
|
+
Encapsulates the possible return values for an agent function.
|
44
|
+
|
45
|
+
Attributes:
|
46
|
+
value (str): The result value as a string.
|
47
|
+
agent (Agent): The agent instance, if applicable.
|
48
|
+
context_variables (dict): A dictionary of context variables.
|
49
|
+
"""
|
50
|
+
value: str = ""
|
51
|
+
agent: Optional[Agent] = None
|
52
|
+
context_variables: dict = {}
|
53
|
+
|
54
|
+
class Tool:
|
55
|
+
def __init__(
|
56
|
+
self,
|
57
|
+
name: str,
|
58
|
+
func: Callable,
|
59
|
+
description: str = "",
|
60
|
+
input_schema: Optional[Dict[str, Any]] = None,
|
61
|
+
dynamic: bool = False,
|
62
|
+
):
|
63
|
+
"""
|
64
|
+
Initialize a Tool object.
|
65
|
+
|
66
|
+
:param name: Name of the tool.
|
67
|
+
:param func: The callable associated with this tool.
|
68
|
+
:param description: A brief description of the tool.
|
69
|
+
:param input_schema: Schema defining the inputs the tool accepts.
|
70
|
+
:param dynamic: Whether this tool is dynamically generated.
|
71
|
+
"""
|
72
|
+
self.name = name
|
73
|
+
self.func = func
|
74
|
+
self.description = description
|
75
|
+
self.input_schema = input_schema or {}
|
76
|
+
self.dynamic = dynamic
|
77
|
+
|
78
|
+
@property
|
79
|
+
def __name__(self):
|
80
|
+
return self.name
|
81
|
+
|
82
|
+
@property
|
83
|
+
def __code__(self):
|
84
|
+
# Return the __code__ of the actual function, or a mock object if missing
|
85
|
+
return getattr(self.func, "__code__", None)
|
86
|
+
|
87
|
+
def __call__(self, *args, **kwargs):
|
88
|
+
"""
|
89
|
+
Make the Tool instance callable.
|
90
|
+
"""
|
91
|
+
return self.func(*args, **kwargs)
|