open-swarm 0.1.1748636259__py3-none-any.whl → 0.1.1748636295__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- open_swarm-0.1.1748636295.dist-info/METADATA +257 -0
- {open_swarm-0.1.1748636259.dist-info → open_swarm-0.1.1748636295.dist-info}/RECORD +24 -17
- swarm/__init__.py +2 -0
- swarm/core.py +411 -0
- swarm/extensions/blueprint/agent_utils.py +40 -16
- swarm/extensions/blueprint/blueprint_base.py +394 -290
- swarm/extensions/blueprint/django_utils.py +79 -181
- swarm/extensions/blueprint/interactive_mode.py +72 -67
- swarm/extensions/blueprint/output_utils.py +22 -35
- swarm/extensions/config/config_loader.py +253 -109
- swarm/extensions/mcp/__init__.py +1 -0
- swarm/extensions/mcp/cache_utils.py +32 -0
- swarm/extensions/mcp/mcp_client.py +233 -0
- swarm/extensions/mcp/mcp_tool_provider.py +135 -0
- swarm/extensions/mcp/mcp_utils.py +260 -0
- swarm/llm/chat_completion.py +19 -48
- swarm/settings.py +106 -70
- swarm/types.py +91 -0
- swarm/views/chat_views.py +38 -31
- swarm/views/utils.py +1 -3
- open_swarm-0.1.1748636259.dist-info/METADATA +0 -188
- {open_swarm-0.1.1748636259.dist-info → open_swarm-0.1.1748636295.dist-info}/WHEEL +0 -0
- {open_swarm-0.1.1748636259.dist-info → open_swarm-0.1.1748636295.dist-info}/entry_points.txt +0 -0
- {open_swarm-0.1.1748636259.dist-info → open_swarm-0.1.1748636295.dist-info}/licenses/LICENSE +0 -0
- {open_swarm-0.1.1748636259.dist-info → open_swarm-0.1.1748636295.dist-info}/top_level.txt +0 -0
swarm/llm/chat_completion.py
CHANGED
@@ -8,16 +8,12 @@ tool call repair, and interaction with the OpenAI API. Located in llm/ for LLM-s
|
|
8
8
|
import os
|
9
9
|
import json
|
10
10
|
import logging
|
11
|
-
from typing import List, Optional, Dict, Any
|
11
|
+
from typing import List, Optional, Dict, Any
|
12
12
|
from collections import defaultdict
|
13
13
|
|
14
14
|
import asyncio
|
15
15
|
from openai import AsyncOpenAI, OpenAIError
|
16
|
-
|
17
|
-
# Assuming it might be part of the base model or a common types module
|
18
|
-
# For now, let's assume it's implicitly handled or use a dict directly
|
19
|
-
# from ..types import ChatCompletionMessage, Agent # If defined in types
|
20
|
-
from ..types import Agent # Import Agent
|
16
|
+
from ..types import ChatCompletionMessage, Agent
|
21
17
|
from ..utils.redact import redact_sensitive_data
|
22
18
|
from ..utils.general_utils import serialize_datetime
|
23
19
|
from ..utils.message_utils import filter_duplicate_system_messages, update_null_content
|
@@ -42,12 +38,10 @@ async def get_chat_completion(
|
|
42
38
|
current_llm_config: Dict[str, Any],
|
43
39
|
max_context_tokens: int,
|
44
40
|
max_context_messages: int,
|
45
|
-
tools: Optional[List[Dict[str, Any]]] = None, # <-- Added tools parameter
|
46
|
-
tool_choice: Optional[str] = "auto", # <-- Added tool_choice parameter
|
47
41
|
model_override: Optional[str] = None,
|
48
42
|
stream: bool = False,
|
49
43
|
debug: bool = False
|
50
|
-
) ->
|
44
|
+
) -> ChatCompletionMessage:
|
51
45
|
"""
|
52
46
|
Retrieve a chat completion from the LLM for the given agent and history.
|
53
47
|
|
@@ -59,14 +53,12 @@ async def get_chat_completion(
|
|
59
53
|
current_llm_config: Current LLM configuration dictionary.
|
60
54
|
max_context_tokens: Maximum token limit for context.
|
61
55
|
max_context_messages: Maximum message limit for context.
|
62
|
-
tools: Optional list of tools in OpenAI format.
|
63
|
-
tool_choice: Tool choice mode (e.g., "auto", "none").
|
64
56
|
model_override: Optional model to use instead of default.
|
65
57
|
stream: If True, stream the response; otherwise, return complete.
|
66
58
|
debug: If True, log detailed debugging information.
|
67
59
|
|
68
60
|
Returns:
|
69
|
-
|
61
|
+
ChatCompletionMessage: The LLM's response message.
|
70
62
|
"""
|
71
63
|
if not agent:
|
72
64
|
logger.error("Cannot generate chat completion: Agent is None")
|
@@ -100,15 +92,10 @@ async def get_chat_completion(
|
|
100
92
|
if "tool_calls" in msg and msg["tool_calls"] is not None and not isinstance(msg["tool_calls"], list):
|
101
93
|
logger.warning(f"Invalid tool_calls in history for '{msg.get('sender', 'unknown')}': {msg['tool_calls']}. Setting to None.")
|
102
94
|
msg["tool_calls"] = None
|
103
|
-
# Ensure content: None becomes content: "" for API compatibility
|
104
|
-
if "content" in msg and msg["content"] is None:
|
105
|
-
msg["content"] = ""
|
106
95
|
messages.append(msg)
|
107
96
|
messages = filter_duplicate_system_messages(messages)
|
108
97
|
messages = truncate_message_history(messages, active_model, max_context_tokens, max_context_messages)
|
109
98
|
messages = repair_message_payload(messages, debug=debug) # Ensure tool calls are paired post-truncation
|
110
|
-
# Final content None -> "" check after repair
|
111
|
-
messages = update_null_content(messages)
|
112
99
|
|
113
100
|
logger.debug(f"Prepared {len(messages)} messages for '{agent.name}'")
|
114
101
|
if debug:
|
@@ -119,45 +106,32 @@ async def get_chat_completion(
|
|
119
106
|
"messages": messages,
|
120
107
|
"stream": stream,
|
121
108
|
"temperature": current_llm_config.get("temperature", 0.7),
|
122
|
-
# --- Pass tools and tool_choice ---
|
123
|
-
"tools": tools if tools else None,
|
124
|
-
"tool_choice": tool_choice if tools else None, # Only set tool_choice if tools are provided
|
125
109
|
}
|
126
110
|
if getattr(agent, "response_format", None):
|
127
111
|
create_params["response_format"] = agent.response_format
|
128
|
-
create_params = {k: v for k, v in create_params.items() if v is not None}
|
129
|
-
|
130
|
-
tool_info_log = f", tools_count={len(tools)}" if tools else ", tools=None"
|
131
|
-
logger.debug(f"Chat completion params: model='{active_model}', messages_count={len(messages)}, stream={stream}{tool_info_log}, tool_choice={create_params.get('tool_choice')}")
|
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}")
|
132
114
|
|
133
115
|
try:
|
134
116
|
logger.debug(f"Calling OpenAI API for '{agent.name}' with model='{active_model}'")
|
135
|
-
# Temporary workaround for potential env var conflicts if client doesn't isolate well
|
136
117
|
prev_openai_api_key = os.environ.pop("OPENAI_API_KEY", None)
|
137
118
|
try:
|
138
119
|
completion = await client.chat.completions.create(**create_params)
|
139
120
|
if stream:
|
140
|
-
return completion
|
141
|
-
|
142
|
-
# --- Handle Non-Streaming Response ---
|
121
|
+
return completion # Return stream object directly
|
143
122
|
if completion.choices and len(completion.choices) > 0 and completion.choices[0].message:
|
144
|
-
|
145
|
-
log_msg = message_dict.get("content", "No content")[:50] if message_dict.get("content") else "No content"
|
146
|
-
if message_dict.get("tool_calls"): log_msg += f" (+{len(message_dict['tool_calls'])} tool calls)"
|
123
|
+
log_msg = completion.choices[0].message.content[:50] if completion.choices[0].message.content else "No content"
|
147
124
|
logger.debug(f"OpenAI completion received for '{agent.name}': {log_msg}...")
|
148
|
-
return
|
125
|
+
return completion.choices[0].message
|
149
126
|
else:
|
150
127
|
logger.warning(f"No valid message in completion for '{agent.name}'")
|
151
|
-
return
|
128
|
+
return ChatCompletionMessage(content="No response generated", role="assistant")
|
152
129
|
finally:
|
153
130
|
if prev_openai_api_key is not None:
|
154
131
|
os.environ["OPENAI_API_KEY"] = prev_openai_api_key
|
155
132
|
except OpenAIError as e:
|
156
133
|
logger.error(f"Chat completion failed for '{agent.name}': {e}")
|
157
134
|
raise
|
158
|
-
except Exception as e: # Catch broader errors during API call
|
159
|
-
logger.error(f"Unexpected error during chat completion for '{agent.name}': {e}", exc_info=True)
|
160
|
-
raise # Re-raise
|
161
135
|
|
162
136
|
|
163
137
|
async def get_chat_completion_message(
|
@@ -168,28 +142,25 @@ async def get_chat_completion_message(
|
|
168
142
|
current_llm_config: Dict[str, Any],
|
169
143
|
max_context_tokens: int,
|
170
144
|
max_context_messages: int,
|
171
|
-
tools: Optional[List[Dict[str, Any]]] = None, # <-- Added tools
|
172
|
-
tool_choice: Optional[str] = "auto", # <-- Added tool_choice
|
173
145
|
model_override: Optional[str] = None,
|
174
146
|
stream: bool = False,
|
175
147
|
debug: bool = False
|
176
|
-
) ->
|
148
|
+
) -> ChatCompletionMessage:
|
177
149
|
"""
|
178
|
-
Wrapper to retrieve and validate a chat completion message
|
150
|
+
Wrapper to retrieve and validate a chat completion message.
|
179
151
|
|
180
152
|
Args:
|
181
153
|
Same as get_chat_completion.
|
182
154
|
|
183
155
|
Returns:
|
184
|
-
|
156
|
+
ChatCompletionMessage: Validated LLM response message.
|
185
157
|
"""
|
186
158
|
logger.debug(f"Fetching chat completion message for '{agent.name}'")
|
187
|
-
|
159
|
+
completion = await get_chat_completion(
|
188
160
|
client, agent, history, context_variables, current_llm_config,
|
189
|
-
max_context_tokens, max_context_messages,
|
190
|
-
tools=tools, tool_choice=tool_choice, # Pass through
|
191
|
-
model_override=model_override, stream=stream, debug=debug
|
161
|
+
max_context_tokens, max_context_messages, model_override, stream, debug
|
192
162
|
)
|
193
|
-
|
194
|
-
|
195
|
-
|
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/settings.py
CHANGED
@@ -1,29 +1,11 @@
|
|
1
1
|
"""
|
2
2
|
Django settings for swarm project.
|
3
|
-
Includes Pydantic base settings for Swarm Core.
|
4
3
|
"""
|
5
4
|
|
6
|
-
import logging
|
7
5
|
import os
|
8
6
|
import sys
|
9
|
-
from enum import Enum
|
10
7
|
from pathlib import Path
|
11
|
-
|
12
|
-
from pydantic_settings import BaseSettings, SettingsConfigDict
|
13
|
-
|
14
|
-
# --- Pydantic Settings for Swarm Core ---
|
15
|
-
class LogFormat(str, Enum):
|
16
|
-
standard = "[%(levelname)s] %(asctime)s - %(name)s:%(lineno)d - %(message)s"
|
17
|
-
simple = "[%(levelname)s] %(name)s - %(message)s"
|
18
|
-
|
19
|
-
class Settings(BaseSettings):
|
20
|
-
model_config = SettingsConfigDict(env_prefix='SWARM_', case_sensitive=False)
|
21
|
-
|
22
|
-
log_level: str = Field(default='INFO', description="Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)")
|
23
|
-
log_format: LogFormat = Field(default=LogFormat.standard, description="Logging format")
|
24
|
-
debug: bool = Field(default=False, description="Global debug flag")
|
25
|
-
|
26
|
-
# --- Standard Django Settings ---
|
8
|
+
import logging
|
27
9
|
|
28
10
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
29
11
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
@@ -38,8 +20,7 @@ TESTING = 'pytest' in sys.modules
|
|
38
20
|
|
39
21
|
# Quick-start development settings - unsuitable for production
|
40
22
|
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY', 'django-insecure-YOUR_FALLBACK_KEY_HERE_CHANGE_ME')
|
41
|
-
|
42
|
-
DEBUG = Settings().debug # Read from instantiated Pydantic settings
|
23
|
+
DEBUG = os.getenv('DEBUG', 'True') == 'True'
|
43
24
|
ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '*').split(',')
|
44
25
|
|
45
26
|
# --- Application definition ---
|
@@ -58,47 +39,59 @@ INSTALLED_APPS = [
|
|
58
39
|
'swarm.apps.SwarmConfig',
|
59
40
|
]
|
60
41
|
|
61
|
-
#
|
62
|
-
#
|
63
|
-
|
64
|
-
#
|
65
|
-
#
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
#
|
70
|
-
|
71
|
-
|
72
|
-
#
|
73
|
-
#
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
#
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
+
|
100
92
|
logging.info(f"Settings: Final INSTALLED_APPS = {INSTALLED_APPS}")
|
101
93
|
|
94
|
+
|
102
95
|
MIDDLEWARE = [
|
103
96
|
'django.middleware.security.SecurityMiddleware',
|
104
97
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
@@ -129,10 +122,18 @@ TEMPLATES = [
|
|
129
122
|
|
130
123
|
WSGI_APPLICATION = 'swarm.wsgi.application'
|
131
124
|
|
125
|
+
# Database
|
132
126
|
SQLITE_DB_PATH = os.getenv('SQLITE_DB_PATH', BASE_DIR / 'db.sqlite3')
|
133
|
-
DATABASES = {
|
127
|
+
DATABASES = {
|
128
|
+
'default': {
|
129
|
+
'ENGINE': 'django.db.backends.sqlite3',
|
130
|
+
'NAME': SQLITE_DB_PATH,
|
131
|
+
}
|
132
|
+
}
|
134
133
|
DJANGO_DATABASE = DATABASES['default']
|
135
134
|
|
135
|
+
|
136
|
+
# Password validation
|
136
137
|
AUTH_PASSWORD_VALIDATORS = [
|
137
138
|
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
|
138
139
|
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
|
@@ -140,13 +141,26 @@ AUTH_PASSWORD_VALIDATORS = [
|
|
140
141
|
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
|
141
142
|
]
|
142
143
|
|
143
|
-
LANGUAGE_CODE = 'en-us'; TIME_ZONE = 'UTC'; USE_I18N = True; USE_TZ = True
|
144
144
|
|
145
|
-
|
146
|
-
|
145
|
+
# Internationalization
|
146
|
+
LANGUAGE_CODE = 'en-us'
|
147
|
+
TIME_ZONE = 'UTC'
|
148
|
+
USE_I18N = True
|
149
|
+
USE_TZ = True
|
150
|
+
|
147
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
|
148
161
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
149
162
|
|
163
|
+
# REST Framework settings
|
150
164
|
REST_FRAMEWORK = {
|
151
165
|
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
152
166
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
@@ -154,7 +168,9 @@ REST_FRAMEWORK = {
|
|
154
168
|
'rest_framework.authentication.TokenAuthentication',
|
155
169
|
'rest_framework.authentication.SessionAuthentication',
|
156
170
|
),
|
157
|
-
'DEFAULT_PERMISSION_CLASSES': (
|
171
|
+
'DEFAULT_PERMISSION_CLASSES': (
|
172
|
+
'rest_framework.permissions.IsAuthenticated',
|
173
|
+
)
|
158
174
|
}
|
159
175
|
|
160
176
|
SPECTACULAR_SETTINGS = {
|
@@ -165,10 +181,22 @@ SPECTACULAR_SETTINGS = {
|
|
165
181
|
'SERVE_PERMISSIONS': ['rest_framework.permissions.AllowAny'],
|
166
182
|
}
|
167
183
|
|
184
|
+
# Logging configuration
|
168
185
|
LOGGING = {
|
169
|
-
'version': 1,
|
170
|
-
'
|
171
|
-
'
|
186
|
+
'version': 1,
|
187
|
+
'disable_existing_loggers': False,
|
188
|
+
'formatters': {
|
189
|
+
'standard': {
|
190
|
+
'format': '[%(levelname)s] %(asctime)s - %(name)s:%(lineno)d - %(message)s'
|
191
|
+
},
|
192
|
+
},
|
193
|
+
'handlers': {
|
194
|
+
'console': {
|
195
|
+
'level': 'DEBUG' if DEBUG else 'INFO',
|
196
|
+
'class': 'logging.StreamHandler',
|
197
|
+
'formatter': 'standard',
|
198
|
+
},
|
199
|
+
},
|
172
200
|
'loggers': {
|
173
201
|
'django': { 'handlers': ['console'], 'level': 'INFO', 'propagate': False, },
|
174
202
|
'django.request': { 'handlers': ['console'], 'level': 'WARNING', 'propagate': False, },
|
@@ -178,12 +206,20 @@ LOGGING = {
|
|
178
206
|
},
|
179
207
|
}
|
180
208
|
|
181
|
-
|
182
|
-
|
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/'
|
183
217
|
|
218
|
+
# Redis settings
|
184
219
|
REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
|
185
220
|
REDIS_PORT = int(os.getenv('REDIS_PORT', 6379))
|
186
221
|
|
222
|
+
# Adjust DB for testing if TESTING flag is set
|
187
223
|
if TESTING:
|
188
224
|
print("Pytest detected: Adjusting settings for testing.")
|
189
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)
|
swarm/views/chat_views.py
CHANGED
@@ -1,76 +1,83 @@
|
|
1
1
|
"""
|
2
2
|
Chat-related views for Open Swarm MCP Core.
|
3
3
|
"""
|
4
|
-
import asyncio
|
5
|
-
import logging
|
6
|
-
import json
|
4
|
+
import asyncio # Import asyncio
|
7
5
|
from django.views.decorators.csrf import csrf_exempt
|
8
6
|
from rest_framework.response import Response
|
9
7
|
from rest_framework.decorators import api_view, authentication_classes, permission_classes
|
10
8
|
from rest_framework.permissions import IsAuthenticated
|
11
9
|
from swarm.auth import EnvOrTokenAuthentication
|
12
10
|
from swarm.utils.logger_setup import setup_logger
|
11
|
+
# Import utils but rename to avoid conflict if this module grows
|
13
12
|
from swarm.views import utils as view_utils
|
14
|
-
from swarm.
|
15
|
-
from swarm.
|
13
|
+
# from swarm.views.utils import parse_chat_request, serialize_swarm_response, get_blueprint_instance
|
14
|
+
# from swarm.views.utils import load_conversation_history, store_conversation_history, run_conversation
|
15
|
+
# from swarm.views.utils import config, llm_config # Import from utils
|
16
16
|
|
17
17
|
logger = setup_logger(__name__)
|
18
18
|
|
19
|
-
#
|
20
|
-
|
19
|
+
# Revert to standard sync def
|
21
20
|
@api_view(['POST'])
|
22
21
|
@csrf_exempt
|
23
22
|
@authentication_classes([EnvOrTokenAuthentication])
|
24
23
|
@permission_classes([IsAuthenticated])
|
25
|
-
def chat_completions(request): #
|
24
|
+
def chat_completions(request): # Mark as sync again
|
26
25
|
"""Handle chat completion requests via POST."""
|
27
26
|
if request.method != "POST":
|
28
27
|
return Response({"error": "Method not allowed. Use POST."}, status=405)
|
29
28
|
logger.info(f"Authenticated User: {request.user}")
|
30
29
|
|
30
|
+
# Use functions from view_utils
|
31
31
|
parse_result = view_utils.parse_chat_request(request)
|
32
|
-
if isinstance(parse_result, Response):
|
32
|
+
if isinstance(parse_result, Response):
|
33
|
+
return parse_result
|
33
34
|
|
34
35
|
body, model, messages, context_vars, conversation_id, tool_call_id = parse_result
|
35
|
-
|
36
|
+
# Use llm_config loaded in utils
|
37
|
+
model_type = "llm" if model in view_utils.llm_config and view_utils.llm_config[model].get("passthrough") else "blueprint"
|
36
38
|
logger.info(f"Identified model type: {model_type} for model: {model}")
|
37
39
|
|
40
|
+
# --- Handle LLM Passthrough directly ---
|
38
41
|
if model_type == "llm":
|
42
|
+
logger.warning(f"LLM Passthrough requested for model '{model}'. This is not yet fully implemented in this view.")
|
43
|
+
# TODO: Implement direct call to Swarm core or LLM client for passthrough
|
39
44
|
return Response({"error": f"LLM passthrough for model '{model}' not implemented."}, status=501)
|
40
45
|
|
41
|
-
|
42
|
-
|
43
|
-
|
46
|
+
# --- Handle Blueprint ---
|
47
|
+
blueprint_instance = view_utils.get_blueprint_instance(model, context_vars)
|
48
|
+
if isinstance(blueprint_instance, Response): # Handle error response from get_blueprint_instance
|
49
|
+
return blueprint_instance
|
50
|
+
if blueprint_instance is None: # Handle case where get_blueprint_instance signaled non-blueprint
|
51
|
+
return Response({"error": f"Model '{model}' is not a loadable blueprint."}, status=404)
|
52
|
+
|
53
|
+
|
54
|
+
messages_extended = view_utils.load_conversation_history(conversation_id, messages, tool_call_id)
|
44
55
|
|
45
|
-
|
46
|
-
#
|
56
|
+
try:
|
57
|
+
# Use asyncio.run() to call the async run_conversation function
|
58
|
+
# This blocks the sync view until the async operation completes.
|
47
59
|
try:
|
48
|
-
logger.debug("Attempting asyncio.run(run_conversation)...")
|
49
60
|
response_obj, updated_context = asyncio.run(
|
50
61
|
view_utils.run_conversation(blueprint_instance, messages_extended, context_vars)
|
51
62
|
)
|
52
|
-
logger.debug("asyncio.run(run_conversation) completed.")
|
53
63
|
except RuntimeError as e:
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
64
|
+
if "cannot be called from a running event loop" in str(e):
|
65
|
+
logger.error("Detected nested asyncio.run call. This can happen in certain test/server setups.")
|
66
|
+
# If already in a loop (e.g., certain test runners or ASGI servers),
|
67
|
+
# you might need a different way to run the async code, like ensure_future
|
68
|
+
# or adapting the server setup. For now, return an error.
|
69
|
+
return Response({"error": "Server configuration error: Nested event loop detected."}, status=500)
|
70
|
+
else:
|
71
|
+
raise e # Reraise other RuntimeErrors
|
59
72
|
|
60
73
|
serialized = view_utils.serialize_swarm_response(response_obj, model, updated_context)
|
61
74
|
|
62
75
|
if conversation_id:
|
63
76
|
serialized["conversation_id"] = conversation_id
|
77
|
+
# Storing history can remain synchronous for now unless it becomes a bottleneck
|
64
78
|
view_utils.store_conversation_history(conversation_id, messages_extended, response_obj)
|
65
79
|
|
66
80
|
return Response(serialized, status=200)
|
67
|
-
|
68
|
-
except FileNotFoundError as e:
|
69
|
-
logger.warning(f"Blueprint not found for model '{model}': {e}")
|
70
|
-
return Response({"error": f"Blueprint not found: {model}"}, status=404)
|
71
|
-
# Catch other exceptions, including the potential RuntimeError from above
|
72
81
|
except Exception as e:
|
73
|
-
logger.error(f"Error during execution
|
74
|
-
|
75
|
-
return Response({"error": f"Error during execution: {error_msg}"}, status=500)
|
76
|
-
|
82
|
+
logger.error(f"Error during execution: {e}", exc_info=True)
|
83
|
+
return Response({"error": f"Error during execution: {str(e)}"}, status=500)
|
swarm/views/utils.py
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
Utility functions for Swarm views.
|
3
3
|
"""
|
4
4
|
import json
|
5
|
-
from swarm.types import ChatMessage
|
6
5
|
import uuid
|
7
6
|
import time
|
8
7
|
import os
|
@@ -358,8 +357,7 @@ def store_conversation_history(conversation_id: str, full_history: List[dict], r
|
|
358
357
|
# Add only messages not already in full_history (prevent duplicates if run_conversation includes input)
|
359
358
|
last_stored_content = json.dumps(history_to_store[-1]) if history_to_store else None
|
360
359
|
for msg in response_messages:
|
361
|
-
|
362
|
-
if json.dumps(msg_dict) != last_stored_content:
|
360
|
+
if json.dumps(msg) != last_stored_content:
|
363
361
|
history_to_store.append(msg)
|
364
362
|
|
365
363
|
|