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/views/utils.py
CHANGED
@@ -1,457 +1,81 @@
|
|
1
|
-
"""
|
2
|
-
Utility functions for Swarm views.
|
3
|
-
"""
|
4
|
-
import json
|
5
|
-
from swarm.types import ChatMessage
|
6
|
-
import uuid
|
7
|
-
import time
|
8
|
-
import os
|
9
|
-
import redis
|
10
1
|
import logging
|
11
|
-
from typing import Any, Dict, List, Optional, Tuple
|
12
|
-
from pathlib import Path
|
13
|
-
|
14
2
|
from django.conf import settings
|
15
|
-
from
|
16
|
-
from rest_framework import status
|
17
|
-
|
18
|
-
# Project-specific imports
|
19
|
-
from swarm.models import ChatConversation, ChatMessage
|
20
|
-
from swarm.extensions.blueprint import discover_blueprints
|
21
|
-
from swarm.extensions.blueprint.blueprint_base import BlueprintBase
|
22
|
-
from swarm.extensions.config.config_loader import load_server_config, load_llm_config
|
23
|
-
from swarm.utils.logger_setup import setup_logger
|
24
|
-
from swarm.utils.redact import redact_sensitive_data
|
25
|
-
from swarm.utils.general_utils import extract_chat_id
|
26
|
-
from swarm.extensions.blueprint.blueprint_utils import filter_blueprints
|
27
|
-
from swarm.settings import BASE_DIR, BLUEPRINTS_DIR # Import BLUEPRINTS_DIR
|
28
|
-
|
29
|
-
logger = setup_logger(__name__)
|
30
|
-
|
31
|
-
# --- Configuration Loading ---
|
32
|
-
CONFIG_PATH = Path(settings.BASE_DIR) / 'swarm_config.json'
|
33
|
-
config = {}
|
34
|
-
llm_config = {}
|
35
|
-
llm_model = "default"
|
36
|
-
llm_provider = "unknown"
|
37
|
-
|
38
|
-
def load_configs():
|
39
|
-
"""Load main and LLM configurations."""
|
40
|
-
global config, llm_config, llm_model, llm_provider
|
41
|
-
try:
|
42
|
-
config = load_server_config(str(CONFIG_PATH))
|
43
|
-
logger.info(f"Server config loaded from {CONFIG_PATH}")
|
44
|
-
except FileNotFoundError:
|
45
|
-
logger.warning(f"Configuration file not found at {CONFIG_PATH}. Using defaults.")
|
46
|
-
config = {} # Use empty dict or default structure if needed
|
47
|
-
except ValueError as e:
|
48
|
-
logger.error(f"Error loading server config: {e}. Using defaults.")
|
49
|
-
config = {}
|
50
|
-
except Exception as e:
|
51
|
-
logger.critical(f"Unexpected error loading server config: {e}", exc_info=True)
|
52
|
-
config = {} # Critical error, use empty config
|
53
|
-
|
54
|
-
try:
|
55
|
-
llm_config = load_llm_config(config) # Load default LLM config
|
56
|
-
llm_model = llm_config.get("model", "default")
|
57
|
-
llm_provider = llm_config.get("provider", "unknown")
|
58
|
-
logger.info(f"Default LLM config loaded: Provider={llm_provider}, Model={llm_model}")
|
59
|
-
except ValueError as e:
|
60
|
-
logger.error(f"Failed to load default LLM configuration: {e}. LLM features may fail.")
|
61
|
-
llm_config = {} # Ensure llm_config is a dict even on error
|
62
|
-
llm_model = "error"
|
63
|
-
llm_provider = "error"
|
64
|
-
except Exception as e:
|
65
|
-
logger.critical(f"Unexpected error loading LLM config: {e}", exc_info=True)
|
66
|
-
llm_config = {}
|
67
|
-
llm_model = "error"
|
68
|
-
llm_provider = "error"
|
69
|
-
|
70
|
-
load_configs() # Load configs when module is imported
|
71
|
-
|
72
|
-
# --- Blueprint Discovery ---
|
73
|
-
blueprints_metadata = {}
|
74
|
-
def discover_and_load_blueprints():
|
75
|
-
"""Discover blueprints from the configured directory."""
|
76
|
-
global blueprints_metadata
|
77
|
-
try:
|
78
|
-
# Ensure BLUEPRINTS_DIR is a Path object
|
79
|
-
bp_dir_path = Path(BLUEPRINTS_DIR) if isinstance(BLUEPRINTS_DIR, str) else BLUEPRINTS_DIR
|
80
|
-
if not bp_dir_path.is_absolute():
|
81
|
-
bp_dir_path = Path(settings.BASE_DIR).parent / bp_dir_path # Assuming relative to project root
|
82
|
-
logger.info(f"Discovering blueprints in: {bp_dir_path}")
|
83
|
-
discovered = discover_blueprints(directories=[str(bp_dir_path)])
|
84
|
-
blueprints_metadata = discovered
|
85
|
-
loaded_names = list(blueprints_metadata.keys())
|
86
|
-
logger.info(f"Discovered blueprints: {loaded_names if loaded_names else 'None'}")
|
87
|
-
except FileNotFoundError:
|
88
|
-
logger.warning(f"Blueprints directory '{BLUEPRINTS_DIR}' not found. No blueprints discovered dynamically.")
|
89
|
-
blueprints_metadata = {}
|
90
|
-
except Exception as e:
|
91
|
-
logger.error(f"Failed during blueprint discovery: {e}", exc_info=True)
|
92
|
-
blueprints_metadata = {}
|
93
|
-
|
94
|
-
discover_and_load_blueprints() # Discover blueprints when module is imported
|
95
|
-
|
96
|
-
# --- Redis Client Initialization ---
|
97
|
-
REDIS_AVAILABLE = bool(os.getenv("STATEFUL_CHAT_ID_PATH")) and hasattr(settings, 'REDIS_HOST') and hasattr(settings, 'REDIS_PORT')
|
98
|
-
redis_client = None
|
99
|
-
if REDIS_AVAILABLE:
|
100
|
-
try:
|
101
|
-
redis_client = redis.Redis(
|
102
|
-
host=settings.REDIS_HOST,
|
103
|
-
port=settings.REDIS_PORT,
|
104
|
-
decode_responses=True
|
105
|
-
)
|
106
|
-
redis_client.ping()
|
107
|
-
logger.info(f"Redis connection successful ({settings.REDIS_HOST}:{settings.REDIS_PORT}).")
|
108
|
-
except Exception as e:
|
109
|
-
logger.warning(f"Redis connection failed: {e}. Stateful chat history via Redis is disabled.")
|
110
|
-
REDIS_AVAILABLE = False
|
111
|
-
else:
|
112
|
-
logger.info("Redis configuration not found or STATEFUL_CHAT_ID_PATH not set. Stateful chat history via Redis is disabled.")
|
113
|
-
|
114
|
-
# --- Helper Functions ---
|
115
|
-
|
116
|
-
def serialize_swarm_response(response: Any, model_name: str, context_variables: Dict[str, Any]) -> Dict[str, Any]:
|
117
|
-
"""Serialize a blueprint response into an OpenAI-compatible chat completion format."""
|
118
|
-
logger.debug(f"Serializing Swarm response, type: {type(response)}, model: {model_name}")
|
119
|
-
|
120
|
-
# Determine messages from response
|
121
|
-
if hasattr(response, 'messages') and isinstance(response.messages, list):
|
122
|
-
messages = response.messages
|
123
|
-
elif isinstance(response, dict) and isinstance(response.get("messages"), list):
|
124
|
-
messages = response.get("messages", [])
|
125
|
-
elif isinstance(response, str):
|
126
|
-
logger.warning(f"Received raw string response, wrapping as assistant message: {response[:100]}...")
|
127
|
-
messages = [{"role": "assistant", "content": response}]
|
128
|
-
else:
|
129
|
-
logger.error(f"Unexpected response type for serialization: {type(response)}. Returning empty response.")
|
130
|
-
messages = []
|
131
|
-
|
132
|
-
# Create choices array based on assistant messages with content
|
133
|
-
choices = []
|
134
|
-
for i, msg in enumerate(messages):
|
135
|
-
if isinstance(msg, dict) and msg.get("role") == "assistant" and msg.get("content") is not None:
|
136
|
-
choice = {
|
137
|
-
"index": len(choices),
|
138
|
-
"message": {
|
139
|
-
"role": "assistant",
|
140
|
-
"content": msg["content"]
|
141
|
-
},
|
142
|
-
"finish_reason": "stop" # Assume stop for non-streaming
|
143
|
-
}
|
144
|
-
# Include tool_calls if present in the original message
|
145
|
-
if msg.get("tool_calls") is not None:
|
146
|
-
choice["message"]["tool_calls"] = msg["tool_calls"]
|
147
|
-
choice["finish_reason"] = "tool_calls" # Adjust finish reason if tools are called
|
148
|
-
|
149
|
-
choices.append(choice)
|
150
|
-
logger.debug(f"Added choice {len(choices)-1}: role={choice['message']['role']}, finish={choice['finish_reason']}")
|
151
|
-
|
152
|
-
if not choices and messages:
|
153
|
-
# Fallback if no assistant message with content, maybe use last message?
|
154
|
-
logger.warning("No assistant messages with content found for 'choices'. Using last message if possible.")
|
155
|
-
# This part might need refinement based on expected behavior for tool-only responses
|
156
|
-
|
157
|
-
# Estimate token usage (basic approximation)
|
158
|
-
prompt_tokens = 0
|
159
|
-
completion_tokens = 0
|
160
|
-
total_tokens = 0
|
161
|
-
# Need access to the *input* messages for prompt_tokens, which aren't passed here.
|
162
|
-
# This usage calculation will be inaccurate without the original prompt.
|
163
|
-
for msg in messages: # Calculating based on response messages only
|
164
|
-
if isinstance(msg, dict):
|
165
|
-
content_tokens = len(str(msg.get("content", "")).split())
|
166
|
-
if msg.get("role") == "assistant":
|
167
|
-
completion_tokens += content_tokens
|
168
|
-
total_tokens += content_tokens
|
169
|
-
if msg.get("tool_calls"): # Add rough estimate for tool call overhead
|
170
|
-
total_tokens += len(json.dumps(msg["tool_calls"])) // 4
|
171
|
-
|
172
|
-
logger.warning("Token usage calculation is approximate and based only on response messages.")
|
173
|
-
|
174
|
-
# Basic serialization structure
|
175
|
-
serialized_response = {
|
176
|
-
"id": f"swarm-chat-{uuid.uuid4()}",
|
177
|
-
"object": "chat.completion",
|
178
|
-
"created": int(time.time()),
|
179
|
-
"model": model_name,
|
180
|
-
"choices": choices,
|
181
|
-
"usage": {
|
182
|
-
"prompt_tokens": prompt_tokens, # Inaccurate without input messages
|
183
|
-
"completion_tokens": completion_tokens,
|
184
|
-
"total_tokens": total_tokens # Inaccurate
|
185
|
-
},
|
186
|
-
# Optionally include context and full response for debugging/state
|
187
|
-
# "context_variables": context_variables,
|
188
|
-
# "full_response": response # Might contain non-serializable objects
|
189
|
-
}
|
190
|
-
|
191
|
-
logger.debug(f"Serialized response: id={serialized_response['id']}, choices={len(choices)}")
|
192
|
-
return serialized_response
|
193
|
-
|
194
|
-
def parse_chat_request(request: Any) -> Any:
|
195
|
-
"""Parse incoming chat completion request body into components."""
|
196
|
-
try:
|
197
|
-
body = json.loads(request.body)
|
198
|
-
model = body.get("model", "default") # Default to 'default' if model not specified
|
199
|
-
messages = body.get("messages", [])
|
200
|
-
|
201
|
-
# Basic validation
|
202
|
-
if not isinstance(messages, list) or not messages:
|
203
|
-
# Try extracting single message if 'messages' is invalid/empty
|
204
|
-
if "message" in body:
|
205
|
-
single_msg = body["message"]
|
206
|
-
if isinstance(single_msg, str): messages = [{"role": "user", "content": single_msg}]
|
207
|
-
elif isinstance(single_msg, dict) and "content" in single_msg:
|
208
|
-
if "role" not in single_msg: single_msg["role"] = "user"
|
209
|
-
messages = [single_msg]
|
210
|
-
else:
|
211
|
-
return Response({"error": "'message' field is invalid."}, status=status.HTTP_400_BAD_REQUEST)
|
212
|
-
else:
|
213
|
-
return Response({"error": "'messages' field is required and must be a non-empty list."}, status=status.HTTP_400_BAD_REQUEST)
|
214
|
-
|
215
|
-
# Ensure all messages have a role (default to user if missing)
|
216
|
-
for msg in messages:
|
217
|
-
if not isinstance(msg, dict) or "content" not in msg:
|
218
|
-
# Allow tool calls without content
|
219
|
-
if not (isinstance(msg, dict) and msg.get("role") == "tool" and msg.get("tool_call_id")):
|
220
|
-
logger.error(f"Invalid message format found: {msg}")
|
221
|
-
return Response({"error": f"Invalid message format: {msg}"}, status=status.HTTP_400_BAD_REQUEST)
|
222
|
-
if "role" not in msg:
|
223
|
-
msg["role"] = "user"
|
224
|
-
|
225
|
-
context_variables = body.get("context_variables", {})
|
226
|
-
if not isinstance(context_variables, dict):
|
227
|
-
logger.warning("Invalid 'context_variables' format, using empty dict.")
|
228
|
-
context_variables = {}
|
229
|
-
|
230
|
-
conversation_id = extract_chat_id(body) or str(uuid.uuid4()) # Generate if not found/extracted
|
231
|
-
|
232
|
-
# Extract last tool_call_id for potential context filtering (optional)
|
233
|
-
tool_call_id = None
|
234
|
-
if messages and isinstance(messages[-1], dict) and messages[-1].get("role") == "tool":
|
235
|
-
tool_call_id = messages[-1].get("tool_call_id")
|
236
|
-
|
237
|
-
logger.debug(f"Parsed request: model={model}, messages_count={len(messages)}, context_keys={list(context_variables.keys())}, conv_id={conversation_id}, tool_id={tool_call_id}")
|
238
|
-
return (body, model, messages, context_variables, conversation_id, tool_call_id)
|
239
|
-
|
240
|
-
except json.JSONDecodeError:
|
241
|
-
logger.error("Invalid JSON payload received.")
|
242
|
-
return Response({"error": "Invalid JSON payload."}, status=status.HTTP_400_BAD_REQUEST)
|
243
|
-
except Exception as e:
|
244
|
-
logger.error(f"Error parsing request: {e}", exc_info=True)
|
245
|
-
return Response({"error": "Failed to parse request."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
246
|
-
|
247
|
-
|
248
|
-
def get_blueprint_instance(model: str, context_vars: dict) -> Any:
|
249
|
-
"""Instantiate a blueprint instance based on the requested model."""
|
250
|
-
logger.debug(f"Attempting to get blueprint instance for model: {model}")
|
251
|
-
# Reload configs and discover blueprints on each request? Or rely on initial load?
|
252
|
-
# Let's rely on initial load for now, assuming blueprints don't change often.
|
253
|
-
# discover_and_load_blueprints() # Uncomment if dynamic reload is needed
|
254
|
-
|
255
|
-
blueprint_meta = blueprints_metadata.get(model)
|
256
|
-
if not blueprint_meta:
|
257
|
-
# Check if it's an LLM passthrough defined in config
|
258
|
-
llm_profile = config.get("llm", {}).get(model)
|
259
|
-
if llm_profile and llm_profile.get("passthrough"):
|
260
|
-
logger.warning(f"Model '{model}' is an LLM passthrough, not a blueprint. Returning None.")
|
261
|
-
# This scenario should ideally be handled before calling get_blueprint_instance
|
262
|
-
# Returning None might cause issues downstream. Consider raising an error
|
263
|
-
# or having the caller handle LLM passthrough separately.
|
264
|
-
# For now, returning None as a signal.
|
265
|
-
return None # Signal it's not a blueprint
|
266
|
-
else:
|
267
|
-
logger.error(f"Model '{model}' not found in discovered blueprints or LLM config.")
|
268
|
-
return Response({"error": f"Model '{model}' not found."}, status=status.HTTP_404_NOT_FOUND)
|
269
|
-
|
270
|
-
blueprint_class = blueprint_meta.get("blueprint_class")
|
271
|
-
if not blueprint_class or not issubclass(blueprint_class, BlueprintBase):
|
272
|
-
logger.error(f"Blueprint class for model '{model}' is invalid or not found.")
|
273
|
-
return Response({"error": f"Blueprint class for model '{model}' is invalid."}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
274
|
-
|
275
|
-
try:
|
276
|
-
# Pass the initially loaded global 'config' to the blueprint instance
|
277
|
-
blueprint_instance = blueprint_class(config=config, debug=settings.DEBUG)
|
278
|
-
logger.info(f"Successfully instantiated blueprint: {model}")
|
279
|
-
# Optionally set active agent based on context, if blueprint supports it
|
280
|
-
# active_agent_name = context_vars.get("active_agent_name")
|
281
|
-
# if active_agent_name and hasattr(blueprint_instance, 'set_active_agent'):
|
282
|
-
# try:
|
283
|
-
# blueprint_instance.set_active_agent(active_agent_name)
|
284
|
-
# except ValueError as e:
|
285
|
-
# logger.warning(f"Could not set active agent '{active_agent_name}': {e}")
|
286
|
-
return blueprint_instance
|
287
|
-
except Exception as e:
|
288
|
-
logger.error(f"Error initializing blueprint '{model}': {e}", exc_info=True)
|
289
|
-
return Response({"error": f"Error initializing blueprint: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
290
|
-
|
291
|
-
def load_conversation_history(conversation_id: Optional[str], current_messages: List[dict], tool_call_id: Optional[str] = None) -> List[dict]:
|
292
|
-
"""Load past messages for a conversation from Redis or database, combined with current."""
|
293
|
-
if not conversation_id:
|
294
|
-
logger.debug("No conversation_id provided, returning only current messages.")
|
295
|
-
return current_messages # Return only the messages from the current request
|
296
|
-
|
297
|
-
past_messages = []
|
298
|
-
# Try Redis first if available
|
299
|
-
if REDIS_AVAILABLE and redis_client:
|
300
|
-
try:
|
301
|
-
history_raw = redis_client.lrange(conversation_id, 0, -1) # Get all items in list
|
302
|
-
if history_raw:
|
303
|
-
# Redis stores list items as strings, parse each JSON string
|
304
|
-
past_messages = [json.loads(msg_str) for msg_str in history_raw]
|
305
|
-
logger.debug(f"Retrieved {len(past_messages)} messages from Redis list for conversation: {conversation_id}")
|
306
|
-
except redis.exceptions.RedisError as e:
|
307
|
-
logger.error(f"Redis error retrieving history for {conversation_id}: {e}", exc_info=True)
|
308
|
-
except json.JSONDecodeError as e:
|
309
|
-
logger.error(f"Error decoding JSON from Redis for {conversation_id}: {e}. History may be corrupted.")
|
310
|
-
# Potentially clear corrupted Redis key?
|
311
|
-
# redis_client.delete(conversation_id)
|
312
|
-
except Exception as e:
|
313
|
-
logger.error(f"Unexpected error retrieving from Redis for {conversation_id}: {e}", exc_info=True)
|
314
|
-
|
315
|
-
# Fallback to Database if Redis fails or history is empty
|
316
|
-
if not past_messages:
|
317
|
-
try:
|
318
|
-
conversation = ChatConversation.objects.filter(conversation_id=conversation_id).first()
|
319
|
-
if conversation:
|
320
|
-
# Query messages related to the conversation
|
321
|
-
query = conversation.messages.all().order_by("timestamp")
|
322
|
-
# Convert DB messages to the expected dict format
|
323
|
-
past_messages = [
|
324
|
-
{"role": msg.sender, "content": msg.content, "tool_calls": json.loads(msg.tool_calls) if msg.tool_calls else None}
|
325
|
-
for msg in query
|
326
|
-
]
|
327
|
-
logger.debug(f"Retrieved {len(past_messages)} messages from DB for conversation: {conversation_id}")
|
328
|
-
else:
|
329
|
-
logger.debug(f"No existing conversation found in DB for ID: {conversation_id}")
|
330
|
-
past_messages = [] # Ensure it's an empty list if no conversation found
|
331
|
-
except Exception as e:
|
332
|
-
logger.error(f"Error retrieving conversation history from DB for {conversation_id}: {e}", exc_info=True)
|
333
|
-
past_messages = [] # Ensure empty list on error
|
334
|
-
|
335
|
-
# Combine history with current request messages
|
336
|
-
# Ensure roles are correct ('user' for human, 'assistant'/'tool' for AI/tools)
|
337
|
-
# Filter out potential duplicates if necessary (e.g., if client resends last user message)
|
338
|
-
combined_messages = past_messages + current_messages
|
339
|
-
logger.debug(f"Combined history: {len(past_messages)} past + {len(current_messages)} current = {len(combined_messages)} total messages for {conversation_id}")
|
340
|
-
return combined_messages
|
3
|
+
from asgiref.sync import sync_to_async, async_to_sync
|
341
4
|
|
5
|
+
# Assuming the discovery functions are correctly located now
|
6
|
+
from swarm.extensions.blueprint.blueprint_discovery import discover_blueprints
|
342
7
|
|
343
|
-
|
344
|
-
"""Store conversation history (including latest response) in DB and/or Redis."""
|
345
|
-
if not conversation_id:
|
346
|
-
logger.error("Cannot store history: conversation_id is missing.")
|
347
|
-
return False
|
8
|
+
logger = logging.getLogger(__name__)
|
348
9
|
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
response_messages = []
|
353
|
-
if hasattr(response_obj, 'messages') and isinstance(response_obj.messages, list):
|
354
|
-
response_messages = response_obj.messages
|
355
|
-
elif isinstance(response_obj, dict) and isinstance(response_obj.get('messages'), list):
|
356
|
-
response_messages = response_obj.get('messages', [])
|
10
|
+
# --- Caching ---
|
11
|
+
_blueprint_meta_cache = None # Cache for the {name: class} mapping
|
12
|
+
_blueprint_instance_cache = {} # Simple instance cache for no-param blueprints
|
357
13
|
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
14
|
+
# --- Blueprint Metadata Loading ---
|
15
|
+
def _load_all_blueprint_metadata_sync():
|
16
|
+
"""Synchronous helper to perform blueprint discovery."""
|
17
|
+
global _blueprint_meta_cache
|
18
|
+
logger.info("Discovering blueprint classes (sync)...")
|
19
|
+
blueprint_classes = discover_blueprints(settings.BLUEPRINT_DIRECTORY)
|
20
|
+
logger.info(f"Found blueprint classes: {list(blueprint_classes.keys())}")
|
21
|
+
_blueprint_meta_cache = blueprint_classes
|
22
|
+
return blueprint_classes
|
364
23
|
|
24
|
+
@sync_to_async
|
25
|
+
def get_available_blueprints():
|
26
|
+
"""Asynchronously retrieves available blueprint classes."""
|
27
|
+
global _blueprint_meta_cache
|
28
|
+
if _blueprint_meta_cache is None:
|
29
|
+
_load_all_blueprint_metadata_sync()
|
30
|
+
return _blueprint_meta_cache
|
365
31
|
|
366
|
-
|
367
|
-
|
368
|
-
# Use update_or_create for conversation to handle creation/retrieval atomically
|
369
|
-
conversation, created = ChatConversation.objects.update_or_create(
|
370
|
-
conversation_id=conversation_id,
|
371
|
-
defaults={'user': None} # Add user association if available (e.g., from request.user)
|
372
|
-
)
|
373
|
-
if created:
|
374
|
-
logger.debug(f"Created new ChatConversation in DB: {conversation_id}")
|
375
|
-
|
376
|
-
# Efficiently store only the messages *added* in this turn (response messages)
|
377
|
-
# Assume `full_history` contains the prompt messages, and `response_messages` contains the response
|
378
|
-
messages_to_add_to_db = []
|
379
|
-
if response_obj:
|
380
|
-
response_msgs_from_obj = getattr(response_obj, 'messages', []) if hasattr(response_obj, 'messages') else response_obj.get('messages', []) if isinstance(response_obj, dict) else []
|
381
|
-
for msg in response_msgs_from_obj:
|
382
|
-
if isinstance(msg, dict):
|
383
|
-
# Basic validation
|
384
|
-
role = msg.get("role")
|
385
|
-
content = msg.get("content")
|
386
|
-
tool_calls = msg.get("tool_calls")
|
387
|
-
if role and (content is not None or tool_calls is not None):
|
388
|
-
messages_to_add_to_db.append(ChatMessage(
|
389
|
-
conversation=conversation,
|
390
|
-
sender=role,
|
391
|
-
content=content,
|
392
|
-
# Store tool_calls as JSON string
|
393
|
-
tool_calls=json.dumps(tool_calls) if tool_calls else None
|
394
|
-
))
|
32
|
+
# --- Blueprint Instance Loading ---
|
33
|
+
# Removed _load_blueprint_class_sync
|
395
34
|
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
logger.debug(f"No new response messages to store in DB for conversation {conversation_id}")
|
401
|
-
|
402
|
-
except Exception as e:
|
403
|
-
logger.error(f"Error storing conversation history to DB for {conversation_id}: {e}", exc_info=True)
|
404
|
-
# Continue to Redis even if DB fails? Or return False? Let's continue for now.
|
35
|
+
async def get_blueprint_instance(blueprint_id: str, params: dict = None):
|
36
|
+
"""Asynchronously gets an instance of a specific blueprint."""
|
37
|
+
logger.debug(f"Getting instance for blueprint: {blueprint_id} with params: {params}")
|
38
|
+
cache_key = (blueprint_id, tuple(sorted(params.items())) if isinstance(params, dict) else params)
|
405
39
|
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
# Use a Redis list (LPUSH/LTRIM or RPUSH/LTRIM for potentially capped history)
|
410
|
-
# For simplicity, let's replace the entire history for now.
|
411
|
-
# Delete existing list and push all new items. Use pipeline for atomicity.
|
412
|
-
pipe = redis_client.pipeline()
|
413
|
-
pipe.delete(conversation_id)
|
414
|
-
# Push each message as a separate JSON string onto the list
|
415
|
-
for msg_dict in history_to_store:
|
416
|
-
pipe.rpush(conversation_id, json.dumps(msg_dict))
|
417
|
-
# Optionally cap the list size
|
418
|
-
# max_redis_history = 100 # Example cap
|
419
|
-
# pipe.ltrim(conversation_id, -max_redis_history, -1)
|
420
|
-
pipe.execute()
|
421
|
-
logger.debug(f"Stored {len(history_to_store)} messages in Redis list for conversation {conversation_id}")
|
422
|
-
except redis.exceptions.RedisError as e:
|
423
|
-
logger.error(f"Redis error storing history list for {conversation_id}: {e}", exc_info=True)
|
424
|
-
return False # Indicate failure if Redis write fails
|
425
|
-
except Exception as e:
|
426
|
-
logger.error(f"Unexpected error storing to Redis for {conversation_id}: {e}", exc_info=True)
|
427
|
-
return False
|
40
|
+
if params is None and blueprint_id in _blueprint_instance_cache:
|
41
|
+
logger.debug(f"Returning cached instance for {blueprint_id}")
|
42
|
+
return _blueprint_instance_cache[blueprint_id]
|
428
43
|
|
429
|
-
|
44
|
+
available_blueprint_classes = await get_available_blueprints()
|
430
45
|
|
46
|
+
if not isinstance(available_blueprint_classes, dict) or blueprint_id not in available_blueprint_classes:
|
47
|
+
logger.error(f"Blueprint ID '{blueprint_id}' not found in available blueprint classes.")
|
48
|
+
return None
|
431
49
|
|
432
|
-
|
433
|
-
"""Run a conversation turn with a blueprint instance asynchronously."""
|
434
|
-
if not isinstance(blueprint_instance, BlueprintBase):
|
435
|
-
# Handle LLM passthrough case if needed, or raise error
|
436
|
-
# For now, assume it must be a BlueprintBase instance
|
437
|
-
logger.error("run_conversation called with non-blueprint instance.")
|
438
|
-
raise TypeError("run_conversation expects a BlueprintBase instance.")
|
50
|
+
blueprint_class = available_blueprint_classes[blueprint_id]
|
439
51
|
|
440
52
|
try:
|
441
|
-
#
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
if
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
return
|
53
|
+
# *** Instantiate the class WITHOUT the params argument ***
|
54
|
+
# If blueprints need params, they should handle it internally
|
55
|
+
# or the base class __init__ needs to accept **kwargs.
|
56
|
+
instance = blueprint_class()
|
57
|
+
logger.info(f"Successfully instantiated blueprint: {blueprint_id}")
|
58
|
+
# Optionally pass params later if needed, e.g., instance.set_params(params) if such a method exists
|
59
|
+
if hasattr(instance, 'set_params') and callable(getattr(instance, 'set_params')):
|
60
|
+
instance.set_params(params) # Example of setting params after init
|
61
|
+
|
62
|
+
if params is None:
|
63
|
+
_blueprint_instance_cache[blueprint_id] = instance
|
64
|
+
return instance
|
453
65
|
except Exception as e:
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
66
|
+
# Catch potential TypeError during instantiation too
|
67
|
+
logger.error(f"Failed to instantiate blueprint class '{blueprint_id}': {e}", exc_info=True)
|
68
|
+
return None
|
69
|
+
|
70
|
+
# --- Model Access Validation ---
|
71
|
+
def validate_model_access(user, model_name):
|
72
|
+
"""Synchronous permission check."""
|
73
|
+
logger.debug(f"Validating access for user '{user}' to model '{model_name}'...")
|
74
|
+
try:
|
75
|
+
available = async_to_sync(get_available_blueprints)()
|
76
|
+
is_available = model_name in available
|
77
|
+
logger.debug(f"Model '{model_name}' availability: {is_available}")
|
78
|
+
return is_available
|
79
|
+
except Exception as e:
|
80
|
+
logger.error(f"Error checking model availability during validation: {e}", exc_info=True)
|
81
|
+
return False
|