open-swarm 0.1.1748636259__py3-none-any.whl → 0.1.1748636455__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.
@@ -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, Union, AsyncGenerator # Added AsyncGenerator
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
- # Make sure ChatCompletionMessage is correctly imported if it's defined elsewhere
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
- ) -> Union[Dict[str, Any], AsyncGenerator[Any, None]]: # Adjusted return type hint
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
- Union[Dict[str, Any], AsyncGenerator[Any, None]]: The LLM's response message (as dict) or stream.
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} # Clean None values
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 # Return stream object directly
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
- message_dict = completion.choices[0].message.model_dump(exclude_none=True)
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 message_dict # Return the message dictionary
125
+ return completion.choices[0].message
149
126
  else:
150
127
  logger.warning(f"No valid message in completion for '{agent.name}'")
151
- return {"role": "assistant", "content": "No response generated"} # Return dict
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
- ) -> Union[Dict[str, Any], AsyncGenerator[Any, None]]: # Return dict or stream
148
+ ) -> ChatCompletionMessage:
177
149
  """
178
- Wrapper to retrieve and validate a chat completion message (returns dict or stream).
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
- Union[Dict[str, Any], AsyncGenerator[Any, None]]: Validated LLM response message as dict or the stream.
156
+ ChatCompletionMessage: Validated LLM response message.
185
157
  """
186
158
  logger.debug(f"Fetching chat completion message for '{agent.name}'")
187
- completion_result = await get_chat_completion(
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
- # If streaming, completion_result is already the generator
194
- # If not streaming, it's the message dictionary
195
- return completion_result
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
- from pydantic import Field # Import Field from Pydantic v2+
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
- # Use the Pydantic setting value for Django's DEBUG
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
- # # --- Conditionally add blueprint apps for TESTING ---
62
- # if TESTING:
63
- # _test_apps_to_add = ['blueprints.university']
64
- # for app in _test_apps_to_add:
65
- # if app not in INSTALLED_APPS:
66
- # INSTALLED_APPS.insert(0, app)
67
- # logging.info(f"Settings [TESTING]: Added '{app}' to INSTALLED_APPS.")
68
- # if 'SWARM_BLUEPRINTS' not in os.environ:
69
- # os.environ['SWARM_BLUEPRINTS'] = 'university'
70
- # logging.info(f"Settings [TESTING]: Set SWARM_BLUEPRINTS='university'")
71
- # else:
72
- # # --- Dynamic App Loading for Production/Development ---
73
- # _INITIAL_BLUEPRINT_APPS = []
74
- # _swarm_blueprints_env = os.getenv('SWARM_BLUEPRINTS')
75
- # _log_source = "Not Set"
76
- # if _swarm_blueprints_env:
77
- # _blueprint_names = [name.strip() for name in _swarm_blueprints_env.split(',') if name.strip()]
78
- # _INITIAL_BLUEPRINT_APPS = [f'blueprints.{name}' for name in _blueprint_names if name.replace('_', '').isidentifier()]
79
- # _log_source = "SWARM_BLUEPRINTS env var"
80
- # logging.info(f"Settings: Found blueprints from env var: {_INITIAL_BLUEPRINT_APPS}")
81
- # else:
82
- # _log_source = "directory scan"
83
- # try:
84
- # if BLUEPRINTS_DIR.is_dir():
85
- # for item in BLUEPRINTS_DIR.iterdir():
86
- # if item.is_dir() and (item / '__init__.py').exists():
87
- # if item.name.replace('_', '').isidentifier():
88
- # _INITIAL_BLUEPRINT_APPS.append(f'blueprints.{item.name}')
89
- # logging.info(f"Settings: Found blueprints from directory scan: {_INITIAL_BLUEPRINT_APPS}")
90
- # except Exception as e:
91
- # logging.error(f"Settings: Error discovering blueprint apps during initial load: {e}")
92
-
93
- # for app in _INITIAL_BLUEPRINT_APPS:
94
- # if app not in INSTALLED_APPS:
95
- # INSTALLED_APPS.append(app)
96
- # logging.info(f"Settings [{_log_source}]: Added '{app}' to INSTALLED_APPS.")
97
- # # --- End App Loading Logic ---
98
-
99
- if isinstance(INSTALLED_APPS, tuple): INSTALLED_APPS = list(INSTALLED_APPS)
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 = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': SQLITE_DB_PATH } }
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
- STATIC_URL = '/static/'; STATIC_ROOT = BASE_DIR / 'staticfiles'
146
- STATICFILES_DIRS = [ BASE_DIR / 'static', BASE_DIR / 'assets' ]
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': ('rest_framework.permissions.IsAuthenticated',)
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, 'disable_existing_loggers': False,
170
- 'formatters': { 'standard': { 'format': '[%(levelname)s] %(asctime)s - %(name)s:%(lineno)d - %(message)s' }, },
171
- 'handlers': { 'console': { 'level': 'DEBUG' if DEBUG else 'INFO', 'class': 'logging.StreamHandler', 'formatter': 'standard', }, },
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
- AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend']
182
- LOGIN_URL = '/accounts/login/'; LOGIN_REDIRECT_URL = '/chatbot/'
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.extensions.config.config_loader import config
15
- from swarm.settings import Settings
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
- # Removed _run_async_in_sync helper
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): # Sync view
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): return parse_result
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
- model_type = "llm" if model in config.get('llm', {}) and config.get('llm', {}).get(model, {}).get("passthrough") else "blueprint"
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
- try:
42
- blueprint_instance = view_utils.get_blueprint_instance(model, context_vars)
43
- messages_extended = view_utils.load_conversation_history(conversation_id, messages, tool_call_id)
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
- # Try running the async function using asyncio.run()
46
- # This might fail in test environments with existing loops.
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
- # Catch potential nested loop errors specifically from asyncio.run()
55
- logger.error(f"Asyncio run error: {e}", exc_info=True)
56
- # Return a 500 error, as the async call couldn't be completed
57
- return Response({"error": f"Server execution error: {str(e)}"}, status=500)
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 for model '{model}': {e}", exc_info=True)
74
- error_msg = str(e) if Settings().debug else "An internal error occurred."
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
- msg_dict = msg.model_dump(exclude_none=True) if isinstance(msg, ChatMessage) else msg
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