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.
@@ -19,77 +19,64 @@ logger = logging.getLogger(__name__)
19
19
 
20
20
  def render_markdown(content: str) -> None:
21
21
  """Render markdown content using rich, if available."""
22
- # --- DEBUG PRINT ---
23
- print(f"\n[DEBUG render_markdown called with rich={RICH_AVAILABLE}]", flush=True)
24
22
  if not RICH_AVAILABLE:
25
- print(content, flush=True) # Fallback print with flush
23
+ print(content)
26
24
  return
27
25
  console = Console()
28
26
  md = Markdown(content)
29
- console.print(md) # Rich handles flushing
27
+ console.print(md)
30
28
 
31
29
  def pretty_print_response(messages: List[Dict[str, Any]], use_markdown: bool = False, spinner=None) -> None:
32
30
  """Format and print messages, optionally rendering assistant content as markdown."""
33
- # --- DEBUG PRINT ---
34
- print(f"\n[DEBUG pretty_print_response called with {len(messages)} messages, use_markdown={use_markdown}]", flush=True)
35
-
36
31
  if spinner:
37
32
  spinner.stop()
38
- sys.stdout.write("\r\033[K") # Clear spinner line
33
+ sys.stdout.write("\r\033[K")
39
34
  sys.stdout.flush()
40
35
 
41
36
  if not messages:
42
37
  logger.debug("No messages to print in pretty_print_response.")
43
38
  return
44
39
 
45
- for i, msg in enumerate(messages):
46
- # --- DEBUG PRINT ---
47
- print(f"\n[DEBUG Processing message {i}: type={type(msg)}]", flush=True)
40
+ for msg in messages:
48
41
  if not isinstance(msg, dict):
49
- print(f"[DEBUG Skipping non-dict message {i}]", flush=True)
50
42
  continue
51
43
 
52
44
  role = msg.get("role")
53
45
  sender = msg.get("sender", role if role else "Unknown")
54
46
  msg_content = msg.get("content")
55
47
  tool_calls = msg.get("tool_calls")
56
- # --- DEBUG PRINT ---
57
- print(f"[DEBUG Message {i}: role={role}, sender={sender}, has_content={bool(msg_content)}, has_tools={bool(tool_calls)}]", flush=True)
58
-
59
48
 
60
49
  if role == "assistant":
61
- print(f"\033[94m{sender}\033[0m: ", end="", flush=True)
50
+ print(f"\033[94m{sender}\033[0m: ", end="")
62
51
  if msg_content:
63
- # --- DEBUG PRINT ---
64
- print(f"\n[DEBUG Assistant content found, printing/rendering... Rich={RICH_AVAILABLE}, Markdown={use_markdown}]", flush=True)
65
52
  if use_markdown and RICH_AVAILABLE:
66
53
  render_markdown(msg_content)
67
54
  else:
68
- # --- DEBUG PRINT ---
69
- print(f"\n[DEBUG Using standard print for content:]", flush=True)
70
- print(msg_content, flush=True) # Added flush
55
+ print(msg_content)
71
56
  elif not tool_calls:
72
- print(flush=True) # Flush newline if no content/tools
57
+ print()
73
58
 
74
59
  if tool_calls and isinstance(tool_calls, list):
75
- print(" \033[92mTool Calls:\033[0m", flush=True)
60
+ print(" \033[92mTool Calls:\033[0m")
76
61
  for tc in tool_calls:
77
- if not isinstance(tc, dict): continue
62
+ if not isinstance(tc, dict):
63
+ continue
78
64
  func = tc.get("function", {})
79
65
  tool_name = func.get("name", "Unnamed Tool")
80
66
  args_str = func.get("arguments", "{}")
81
- try: args_obj = json.loads(args_str); args_pretty = ", ".join(f"{k}={v!r}" for k, v in args_obj.items())
82
- except json.JSONDecodeError: args_pretty = args_str
83
- print(f" \033[95m{tool_name}\033[0m({args_pretty})", flush=True)
67
+ try:
68
+ args_obj = json.loads(args_str)
69
+ args_pretty = ", ".join(f"{k}={v!r}" for k, v in args_obj.items())
70
+ except json.JSONDecodeError:
71
+ args_pretty = args_str
72
+ print(f" \033[95m{tool_name}\033[0m({args_pretty})")
84
73
 
85
74
  elif role == "tool":
86
75
  tool_name = msg.get("tool_name", msg.get("name", "tool"))
87
76
  tool_id = msg.get("tool_call_id", "N/A")
88
- try: content_obj = json.loads(msg_content); pretty_content = json.dumps(content_obj, indent=2)
89
- except (json.JSONDecodeError, TypeError): pretty_content = msg_content
90
- print(f" \033[93m[{tool_name} Result ID: {tool_id}]\033[0m:\n {pretty_content.replace(chr(10), chr(10) + ' ')}", flush=True)
91
- else:
92
- # --- DEBUG PRINT ---
93
- print(f"[DEBUG Skipping message {i} with role '{role}']", flush=True)
94
-
95
-
77
+ try:
78
+ content_obj = json.loads(msg_content)
79
+ pretty_content = json.dumps(content_obj, indent=2)
80
+ except (json.JSONDecodeError, TypeError):
81
+ pretty_content = msg_content
82
+ print(f" \033[93m[{tool_name} Result ID: {tool_id}]\033[0m:\n {pretty_content.replace(chr(10), chr(10) + ' ')}")
@@ -9,27 +9,19 @@ import logging
9
9
  from typing import Any, Dict, List, Tuple, Optional
10
10
  from pathlib import Path
11
11
  from dotenv import load_dotenv
12
+ # Import save_server_config carefully
12
13
  try: from .server_config import save_server_config
13
14
  except ImportError: save_server_config = None
14
-
15
- SWARM_DEBUG = os.getenv("SWARM_DEBUG", "False").lower() in ("true", "1", "yes")
16
- try: from swarm.settings import BASE_DIR
17
- except ImportError: BASE_DIR = Path(__file__).resolve().parent.parent.parent
18
-
15
+ from swarm.settings import DEBUG, BASE_DIR
19
16
  from swarm.utils.redact import redact_sensitive_data
20
17
 
21
18
  logger = logging.getLogger(__name__)
22
- logger.setLevel(logging.DEBUG if SWARM_DEBUG else logging.INFO)
23
-
24
- # Add handler only if needed, DO NOT set handler level conditionally here
25
- if not logger.handlers and not logging.getLogger().hasHandlers():
19
+ logger.setLevel(logging.DEBUG if DEBUG else logging.INFO)
20
+ if not logger.handlers:
26
21
  stream_handler = logging.StreamHandler()
27
22
  formatter = logging.Formatter("[%(levelname)s] %(asctime)s - %(name)s:%(lineno)d - %(message)s")
28
23
  stream_handler.setFormatter(formatter)
29
24
  logger.addHandler(stream_handler)
30
- # --- REMOVED CONDITIONAL HANDLER LEVEL SETTING ---
31
- # if not SWARM_DEBUG:
32
- # stream_handler.setLevel(logging.WARNING)
33
25
 
34
26
  config: Dict[str, Any] = {}
35
27
  load_dotenv()
@@ -40,54 +32,68 @@ def process_config(config_dict: dict) -> dict:
40
32
  try:
41
33
  resolved_config = resolve_placeholders(config_dict)
42
34
  if logger.isEnabledFor(logging.DEBUG): logger.debug("Config after resolving placeholders: " + json.dumps(redact_sensitive_data(resolved_config), indent=2))
35
+
43
36
  disable_merge = os.getenv("DISABLE_MCP_MERGE", "false").lower() in ("true", "1", "yes")
44
37
  if not disable_merge:
45
38
  if os.name == "nt": external_mcp_path = Path(os.getenv("APPDATA", Path.home())) / "Claude" / "claude_desktop_config.json"
46
39
  else: external_mcp_path = Path.home() / ".vscode-server" / "data" / "User" / "globalStorage" / "rooveterinaryinc.roo-cline" / "settings" / "cline_mcp_settings.json"
40
+
47
41
  if external_mcp_path.exists():
48
42
  logger.info(f"Found external MCP settings file at: {external_mcp_path}")
49
43
  try:
50
- with open(external_mcp_path, "r", encoding='utf-8') as mcp_file: external_mcp_config = json.load(mcp_file)
44
+ with open(external_mcp_path, "r") as mcp_file: external_mcp_config = json.load(mcp_file)
51
45
  if logger.isEnabledFor(logging.DEBUG): logger.debug("Loaded external MCP settings: " + json.dumps(redact_sensitive_data(external_mcp_config), indent=2))
52
- main_mcp_servers = resolved_config.get("mcpServers", {}); external_mcp_servers = external_mcp_config.get("mcpServers", {})
53
- merged_mcp_servers = main_mcp_servers.copy(); servers_added_count = 0
54
- for name, server_cfg in external_mcp_servers.items():
55
- if name not in merged_mcp_servers and not server_cfg.get("disabled", False): merged_mcp_servers[name] = server_cfg; servers_added_count += 1
56
- if servers_added_count > 0: resolved_config["mcpServers"] = merged_mcp_servers; logger.info(f"Merged {servers_added_count} MCP servers.");
46
+
47
+ main_mcp_servers = resolved_config.get("mcpServers", {})
48
+ external_mcp_servers = external_mcp_config.get("mcpServers", {})
49
+ merged_mcp_servers = main_mcp_servers.copy()
50
+ servers_added_count = 0
51
+ for server_name, server_config in external_mcp_servers.items():
52
+ if server_name not in merged_mcp_servers and not server_config.get("disabled", False):
53
+ merged_mcp_servers[server_name] = server_config
54
+ servers_added_count += 1
55
+ if servers_added_count > 0:
56
+ resolved_config["mcpServers"] = merged_mcp_servers
57
+ logger.info(f"Merged {servers_added_count} MCP servers from external settings.")
58
+ if logger.isEnabledFor(logging.DEBUG): logger.debug("Merged MCP servers config: " + json.dumps(redact_sensitive_data(merged_mcp_servers), indent=2))
57
59
  else: logger.debug("No new MCP servers added from external settings.")
58
60
  except Exception as merge_err: logger.error(f"Failed to load/merge MCP settings from '{external_mcp_path}': {merge_err}", exc_info=logger.isEnabledFor(logging.DEBUG))
59
61
  else: logger.debug(f"External MCP settings file not found at {external_mcp_path}. Skipping merge.")
60
- else: logger.debug("MCP settings merge disabled.")
61
- except Exception as e: logger.error(f"Failed during config processing: {e}", exc_info=logger.isEnabledFor(logging.DEBUG)); raise
62
+ else: logger.debug("MCP settings merge disabled via DISABLE_MCP_MERGE env var.")
63
+ except Exception as e: logger.error(f"Failed during configuration processing: {e}", exc_info=logger.isEnabledFor(logging.DEBUG)); raise
62
64
  globals()["config"] = resolved_config
63
65
  return resolved_config
64
66
 
65
67
  def resolve_placeholders(obj: Any) -> Any:
66
- """Recursively resolve ${VAR_NAME} placeholders."""
68
+ """Recursively resolve ${VAR_NAME} placeholders. Returns None if var not found."""
67
69
  if isinstance(obj, dict): return {k: resolve_placeholders(v) for k, v in obj.items()}
68
70
  elif isinstance(obj, list): return [resolve_placeholders(item) for item in obj]
69
71
  elif isinstance(obj, str):
70
- pattern = re.compile(r'\$\{(\w+(?:[_-]\w+)*)\}')
71
- resolved_string = obj; any_unresolved = False
72
- for var_name in pattern.findall(obj):
73
- env_value = os.getenv(var_name); placeholder = f'${{{var_name}}}'
72
+ pattern = re.compile(r'\$\{(\w+)\}')
73
+ resolved_string = obj
74
+ placeholders_found = pattern.findall(obj)
75
+ all_resolved = True # Flag to track if all placeholders in string were resolved
76
+ for var_name in placeholders_found:
77
+ env_value = os.getenv(var_name)
78
+ placeholder = f'${{{var_name}}}'
74
79
  if env_value is None:
75
- log_level = logging.DEBUG
80
+ logger.warning(f"Env var '{var_name}' not set for placeholder '{placeholder}'. Placeholder will resolve to None.")
81
+ # If only a placeholder exists, return None directly
76
82
  if resolved_string == placeholder:
77
- log_level = logging.WARNING
78
- resolved_string = None
79
- any_unresolved = True
80
- logger.log(log_level, f"Env var '{var_name}' not set for placeholder '{placeholder}'. Resolving to None.")
81
83
  return None
82
- else:
83
- resolved_string = resolved_string.replace(placeholder, "")
84
- any_unresolved = True
85
- logger.log(log_level, f"Env var '{var_name}' not set for placeholder '{placeholder}'. Removing from string.")
84
+ # If placeholder is part of larger string, replace with empty string or marker?
85
+ # Let's replace with empty string for now to avoid partial resolution issues.
86
+ resolved_string = resolved_string.replace(placeholder, "")
87
+ all_resolved = False # Mark that not all placeholders resolved fully
86
88
  else:
87
- resolved_string = resolved_string.replace(placeholder, env_value)
88
- if logger.isEnabledFor(logging.DEBUG): logger.debug(f"Resolved placeholder '{placeholder}' using env var '{var_name}'.")
89
- if any_unresolved and resolved_string is not None:
90
- logger.debug(f"String '{obj}' contained unresolved placeholders. Result: '{resolved_string}'")
89
+ resolved_string = resolved_string.replace(placeholder, env_value)
90
+ if logger.isEnabledFor(logging.DEBUG): logger.debug(f"Resolved placeholder '{placeholder}' using env var '{var_name}'.")
91
+
92
+ # If any placeholder failed to resolve in a mixed string, log it.
93
+ # If the original string was *only* an unresolved placeholder, we already returned None.
94
+ if not all_resolved and len(placeholders_found) > 0:
95
+ logger.warning(f"String '{obj}' contained unresolved placeholders. Result: '{resolved_string}'")
96
+
91
97
  return resolved_string
92
98
  else: return obj
93
99
 
@@ -97,86 +103,238 @@ def load_server_config(file_path: Optional[str] = None) -> dict:
97
103
  if file_path:
98
104
  path_obj = Path(file_path)
99
105
  if path_obj.is_file(): config_path = path_obj; logger.info(f"Using provided config file path: {config_path}")
100
- else: logger.warning(f"Provided path '{file_path}' not found. Searching standard locations.")
106
+ else: logger.warning(f"Provided path '{file_path}' not found/not file. Searching standard locations.")
101
107
  if not config_path:
102
- standard_paths = [ Path.cwd() / "swarm_config.json", Path(BASE_DIR) / "swarm_config.json", Path.home() / ".swarm" / "swarm_config.json" ]
103
- config_path = next((p for p in standard_paths if p.is_file()), None)
104
- if not config_path: raise FileNotFoundError(f"Config file 'swarm_config.json' not found. Checked: {[str(p) for p in standard_paths]}")
105
- logger.info(f"Using config file found at: {config_path}")
108
+ current_dir = Path.cwd()
109
+ standard_paths = [ current_dir / "swarm_config.json", Path(BASE_DIR) / "swarm_config.json", Path.home() / ".swarm" / "swarm_config.json" ]
110
+ for candidate in standard_paths:
111
+ if candidate.is_file(): config_path = candidate; logger.info(f"Using config file found at: {config_path}"); break
112
+ if not config_path: raise FileNotFoundError(f"Config file 'swarm_config.json' not found in provided path or standard locations: {[str(p) for p in standard_paths]}")
113
+ logger.debug(f"Attempting to load config from: {config_path}")
106
114
  try:
115
+ # Ensure reading with UTF-8 encoding
107
116
  raw_config = json.loads(config_path.read_text(encoding='utf-8'))
108
117
  if logger.isEnabledFor(logging.DEBUG): logger.debug(f"Raw config loaded: {redact_sensitive_data(raw_config)}")
109
- processed_config = process_config(raw_config)
110
- globals()["config"] = processed_config
111
- logger.info(f"Config loaded and processed from {config_path}")
112
- return processed_config
113
- except json.JSONDecodeError as e: logger.critical(f"Invalid JSON in {config_path}: {e}"); raise ValueError(f"Invalid JSON") from e
114
- except Exception as e: logger.critical(f"Failed to read/process config {config_path}: {e}"); raise ValueError("Failed to load/process config") from e
118
+ except json.JSONDecodeError as json_err:
119
+ logger.critical(f"Invalid JSON in config file {config_path}: {json_err}")
120
+ raise ValueError(f"Invalid JSON in config {config_path}: {json_err}") from json_err
121
+ except Exception as load_err:
122
+ logger.critical(f"Failed to read config file {config_path}: {load_err}")
123
+ raise ValueError(f"Failed to read config {config_path}") from load_err
124
+ try:
125
+ processed_config = process_config(raw_config)
126
+ globals()["config"] = processed_config
127
+ logger.info(f"Config loaded and processed from {config_path}")
128
+ return processed_config
129
+ except Exception as process_err: logger.critical(f"Failed to process config from {config_path}: {process_err}", exc_info=True); raise ValueError(f"Failed to process config from {config_path}") from process_err
130
+
131
+ # --- Start of Missing Functions ---
132
+
133
+ def are_required_mcp_servers_configured(required_servers: List[str], config_dict: Dict[str, Any]) -> Tuple[bool, List[str]]:
134
+ """Checks if required MCP servers are present in the config."""
135
+ if not required_servers: return True, []
136
+ mcp_servers = config_dict.get("mcpServers", {})
137
+ if not isinstance(mcp_servers, dict):
138
+ logger.warning("MCP servers configuration ('mcpServers') is missing or invalid.")
139
+ return False, required_servers # All are missing if section is invalid
140
+
141
+ missing = [server for server in required_servers if server not in mcp_servers]
142
+ if missing:
143
+ logger.warning(f"Required MCP servers are missing from configuration: {missing}")
144
+ return False, missing
145
+ else:
146
+ logger.debug("All required MCP servers are configured.")
147
+ return True, []
148
+
149
+ def validate_mcp_server_env(mcp_servers: Dict[str, Any], required_servers: Optional[List[str]] = None) -> None:
150
+ """
151
+ Validates that required environment variables specified within MCP server
152
+ configurations are actually set in the environment. Assumes placeholders in
153
+ the config's `env` section values are *already resolved* before calling this.
154
+
155
+ Args:
156
+ mcp_servers: Dictionary of MCP server configurations (placeholders resolved).
157
+ required_servers: Optional list of specific server names to validate. If None, validates all.
158
+
159
+ Raises:
160
+ ValueError: If a required environment variable for a validated server is not set.
161
+ """
162
+ servers_to_validate = mcp_servers
163
+ if required_servers is not None:
164
+ servers_to_validate = {k: v for k, v in mcp_servers.items() if k in required_servers}
165
+ missing_keys = [k for k in required_servers if k not in mcp_servers]
166
+ if missing_keys: logger.warning(f"Required MCP servers missing during env validation: {missing_keys}")
167
+
168
+ logger.debug(f"Validating environment variables for MCP servers: {list(servers_to_validate.keys())}")
169
+
170
+ for server_name, server_config in servers_to_validate.items():
171
+ env_section = server_config.get("env", {})
172
+ if not isinstance(env_section, dict): logger.warning(f"'env' for MCP server '{server_name}' invalid. Skipping."); continue
173
+ logger.debug(f"Validating env for MCP server '{server_name}'.")
174
+ for env_key, env_spec in env_section.items():
175
+ # Determine if required (default is True)
176
+ is_required = env_spec.get("required", True) if isinstance(env_spec, dict) else True
177
+ if not is_required: logger.debug(f"Skipping optional env var '{env_key}' for '{server_name}'."); continue
178
+
179
+ # Get the RESOLVED value from the config dict
180
+ config_value = env_spec.get("value") if isinstance(env_spec, dict) else env_spec
181
+
182
+ # Check if the resolved value is missing or empty
183
+ if config_value is None or (isinstance(config_value, str) and not config_value.strip()):
184
+ # This check assumes resolve_placeholders returned None or empty for missing env vars
185
+ raise ValueError(f"Required env var '{env_key}' for MCP server '{server_name}' is missing or empty in resolved config.")
186
+ else: logger.debug(f"Env var '{env_key}' for '{server_name}' present in resolved config.")
187
+
188
+ def get_default_llm_config(config_dict: Dict[str, Any]) -> Dict[str, Any]:
189
+ """Retrieves the config dict for the default LLM profile."""
190
+ selected_llm_name = os.getenv("DEFAULT_LLM", "default")
191
+ logger.debug(f"Getting default LLM config for profile: '{selected_llm_name}'")
192
+ llm_profiles = config_dict.get("llm", {})
193
+ if not isinstance(llm_profiles, dict): raise ValueError("'llm' section missing or invalid.")
194
+ llm_config = llm_profiles.get(selected_llm_name)
195
+ if not llm_config:
196
+ if selected_llm_name != "default" and "default" in llm_profiles:
197
+ logger.warning(f"Profile '{selected_llm_name}' not found, falling back to 'default'.")
198
+ llm_config = llm_profiles.get("default")
199
+ if not llm_config: # Guard against empty 'default'
200
+ raise ValueError(f"LLM profile '{selected_llm_name}' not found and 'default' profile is missing or invalid.")
201
+ else: raise ValueError(f"LLM profile '{selected_llm_name}' (nor 'default') not found.")
202
+ if not isinstance(llm_config, dict): raise ValueError(f"LLM profile '{selected_llm_name}' invalid.")
203
+ if logger.isEnabledFor(logging.DEBUG): logger.debug(f"Using LLM profile '{selected_llm_name}': {redact_sensitive_data(llm_config)}")
204
+ return llm_config
205
+
206
+ def validate_api_keys(config_dict: Dict[str, Any], selected_llm: str = "default") -> Dict[str, Any]:
207
+ """Validates API key presence for a selected LLM profile (called by load_llm_config)."""
208
+ logger.debug(f"Validating API keys for LLM profile '{selected_llm}'.")
209
+ llm_profiles = config_dict.get("llm", {})
210
+ if not isinstance(llm_profiles, dict): logger.warning("No 'llm' section found, skipping API key validation."); return config_dict
211
+ llm_config = llm_profiles.get(selected_llm)
212
+ if not isinstance(llm_config, dict): logger.warning(f"No config for LLM profile '{selected_llm}', skipping validation."); return config_dict
213
+
214
+ api_key_required = llm_config.get("api_key_required", True)
215
+ api_key = llm_config.get("api_key")
216
+ # Use the fact that resolve_placeholders now returns None for missing env vars
217
+ key_is_missing_or_empty = api_key is None or (isinstance(api_key, str) and not api_key.strip())
218
+
219
+ if api_key_required and key_is_missing_or_empty:
220
+ # If the key is missing/empty *after* resolving placeholders, it means
221
+ # neither the config nor the specific env var had it.
222
+ # Check OPENAI_API_KEY as a general fallback ONLY IF not found specifically.
223
+ common_fallback_var = "OPENAI_API_KEY"
224
+ fallback_key = os.getenv(common_fallback_var)
225
+
226
+ specific_env_var_name = f"{selected_llm.upper()}_API_KEY" # e.g., OPENAI_API_KEY, ANTHROPIC_API_KEY
227
+
228
+ # Check specific env var first
229
+ specific_key = os.getenv(specific_env_var_name)
230
+ if specific_key:
231
+ logger.info(f"API key missing/empty in resolved config for '{selected_llm}', using env var '{specific_env_var_name}'.")
232
+ # Update the config dict in place (or return a modified copy if preferred)
233
+ llm_config["api_key"] = specific_key
234
+ elif fallback_key:
235
+ logger.info(f"API key missing/empty for '{selected_llm}' and specific env var '{specific_env_var_name}' not set. Using fallback env var '{common_fallback_var}'.")
236
+ llm_config["api_key"] = fallback_key
237
+ else:
238
+ raise ValueError(f"Required API key for LLM profile '{selected_llm}' is missing or empty. Checked config, env var '{specific_env_var_name}', and fallback '{common_fallback_var}'.")
239
+
240
+ elif api_key_required: logger.debug(f"API key validation successful for '{selected_llm}'.")
241
+ else: logger.debug(f"API key not required for '{selected_llm}'.")
242
+ # Return the potentially modified config_dict (or just llm_config part if preferred)
243
+ return config_dict
244
+
245
+
246
+ def validate_and_select_llm_provider(config_dict: Dict[str, Any]) -> Dict[str, Any]:
247
+ """Validates the selected LLM provider and returns its config."""
248
+ logger.debug("Validating and selecting LLM provider based on DEFAULT_LLM.")
249
+ try:
250
+ llm_name = os.getenv("DEFAULT_LLM", "default")
251
+ llm_config = load_llm_config(config_dict, llm_name) # Use load_llm_config which includes validation
252
+ logger.debug(f"LLM provider '{llm_name}' validated successfully.")
253
+ return llm_config
254
+ except ValueError as e: logger.error(f"LLM provider validation failed: {e}"); raise
255
+
256
+ def inject_env_vars(config_dict: Dict[str, Any]) -> Dict[str, Any]:
257
+ """Ensures placeholders are resolved (delegates to resolve_placeholders)."""
258
+ logger.debug("Ensuring environment variable placeholders are resolved.")
259
+ return resolve_placeholders(config_dict)
115
260
 
116
261
  def load_llm_config(config_dict: Optional[Dict[str, Any]] = None, llm_name: Optional[str] = None) -> Dict[str, Any]:
117
- """Loads, validates, and resolves API keys for a specific LLM profile."""
262
+ """Loads and validates config for a specific LLM profile."""
118
263
  if config_dict is None:
264
+ # Try loading from global if not provided
119
265
  global_config = globals().get("config")
120
266
  if not global_config:
121
267
  try: config_dict = load_server_config(); globals()["config"] = config_dict
122
268
  except Exception as e: raise ValueError("Global config not loaded and no config_dict provided.") from e
123
- else: config_dict = global_config
269
+ else:
270
+ config_dict = global_config
124
271
 
125
272
  target_llm_name = llm_name or os.getenv("DEFAULT_LLM", "default")
126
- logger.debug(f"LOAD_LLM: Loading profile: '{target_llm_name}'.")
127
-
273
+ logger.debug(f"Loading LLM config for profile: '{target_llm_name}'")
274
+ # Resolve placeholders FIRST using the provided or loaded config_dict
128
275
  resolved_config = resolve_placeholders(config_dict)
276
+
129
277
  llm_profiles = resolved_config.get("llm", {})
130
278
  if not isinstance(llm_profiles, dict): raise ValueError("'llm' section must be a dictionary.")
131
-
132
279
  llm_config = llm_profiles.get(target_llm_name)
133
- config_source = f"config file ('{target_llm_name}')"
134
- logger.debug(f"LOAD_LLM: Initial lookup for '{target_llm_name}': {'Found' if llm_config else 'Missing'}")
135
280
 
281
+ # Fallback Logic (if profile not found after resolving)
136
282
  if not llm_config:
137
- logger.warning(f"LOAD_LLM: Config for '{target_llm_name}' not found. Generating fallback.")
138
- config_source = "fallback generation"
283
+ logger.warning(f"LLM config for '{target_llm_name}' not found. Generating fallback.")
139
284
  fb_provider = os.getenv("DEFAULT_LLM_PROVIDER", "openai"); fb_model = os.getenv("DEFAULT_LLM_MODEL", "gpt-4o")
140
- llm_config = { "provider": fb_provider, "model": fb_model, "api_key": None, "base_url": None }
141
- logger.debug(f"LOAD_LLM: Generated fallback core config: {llm_config}")
285
+ # Check env vars for fallback API key *after* trying the specific one based on target_llm_name
286
+ specific_env_key = os.getenv(f"{target_llm_name.upper()}_API_KEY")
287
+ openai_env_key = os.getenv("OPENAI_API_KEY")
288
+ fb_api_key = specific_env_key or openai_env_key or "" # Use specific, then openai, then empty
289
+
290
+ specific_env_url = os.getenv(f"{target_llm_name.upper()}_BASE_URL")
291
+ openai_env_url = os.getenv("OPENAI_API_BASE")
292
+ default_openai_url = "https://api.openai.com/v1" if fb_provider == "openai" else None
293
+ fb_base_url = specific_env_url or openai_env_url or default_openai_url
294
+
295
+ llm_config = {k: v for k, v in {
296
+ "provider": fb_provider,
297
+ "model": fb_model,
298
+ "base_url": fb_base_url,
299
+ "api_key": fb_api_key, # Use the determined fallback key
300
+ # Determine requirement based on provider (adjust providers as needed)
301
+ "api_key_required": fb_provider not in ["ollama", "lmstudio", "groq"] # Example: groq might need key
302
+ }.items() if v is not None}
303
+ logger.debug(f"Using fallback config for '{target_llm_name}': {redact_sensitive_data(llm_config)}")
142
304
 
143
305
  if not isinstance(llm_config, dict): raise ValueError(f"LLM profile '{target_llm_name}' must be a dictionary.")
144
306
 
145
- final_api_key = llm_config.get("api_key"); key_log_source = f"{config_source} (resolved)" if final_api_key else config_source
146
- provider = llm_config.get("provider"); api_key_required = llm_config.get("api_key_required", True)
147
- logger.debug(f"LOAD_LLM: Initial key from {key_log_source}: {'****' if final_api_key else 'None'}. Required={api_key_required}")
148
-
149
- logger.debug(f"LOAD_LLM: Checking ENV vars for potential override.")
150
- specific_env_var_name = f"{provider.upper()}_API_KEY" if provider else "PROVIDER_API_KEY"
151
- common_fallback_var = "OPENAI_API_KEY"
152
- specific_key_from_env = os.getenv(specific_env_var_name); fallback_key_from_env = os.getenv(common_fallback_var)
153
- logger.debug(f"LOAD_LLM: Env Check: Specific ('{specific_env_var_name}')={'****' if specific_key_from_env else 'None'}, Fallback ('{common_fallback_var}')={'****' if fallback_key_from_env else 'None'}")
154
-
155
- if specific_key_from_env:
156
- if final_api_key != specific_key_from_env: logger.info(f"LOAD_LLM: Overriding key with env var '{specific_env_var_name}'.")
157
- final_api_key = specific_key_from_env; key_log_source = f"env var '{specific_env_var_name}'"
158
- elif fallback_key_from_env:
159
- if not specific_key_from_env or specific_env_var_name == common_fallback_var:
160
- if final_api_key != fallback_key_from_env: logger.info(f"LOAD_LLM: Overriding key with fallback env var '{common_fallback_var}'.")
161
- final_api_key = fallback_key_from_env; key_log_source = f"env var '{common_fallback_var}'"
162
- else: logger.debug(f"LOAD_LLM: Specific env key '{specific_env_var_name}' unset, NOT using fallback.")
163
- else: logger.debug(f"LOAD_LLM: No relevant API key found in environment variables.")
164
-
165
- key_is_still_missing_or_empty = final_api_key is None or (isinstance(final_api_key, str) and not final_api_key.strip())
166
- logger.debug(f"LOAD_LLM: Key after env check: {'****' if final_api_key else 'None'}. Source: {key_log_source}. Still MissingOrEmpty={key_is_still_missing_or_empty}")
167
-
168
- if key_is_still_missing_or_empty:
169
- if api_key_required and not os.getenv("SUPPRESS_DUMMY_KEY"):
170
- final_api_key = "sk-DUMMYKEY"; key_log_source = "dummy key"; logger.warning(f"LOAD_LLM: Applying dummy key for '{target_llm_name}'.")
171
- elif api_key_required:
172
- key_log_source = "MISSING - ERROR"; raise ValueError(f"Required API key for LLM profile '{target_llm_name}' is missing.")
173
- else: key_log_source = "Not Required/Not Found"
174
-
175
- final_llm_config = llm_config.copy(); final_llm_config["api_key"] = final_api_key; final_llm_config["_log_key_source"] = key_log_source
176
- logger.debug(f"LOAD_LLM: Returning final config for '{target_llm_name}': {redact_sensitive_data(final_llm_config)}")
177
- return final_llm_config
307
+ # --- API Key Validation integrated here ---
308
+ api_key_required = llm_config.get("api_key_required", True)
309
+ # Check the api_key *within the potentially generated or loaded llm_config*
310
+ api_key = llm_config.get("api_key")
311
+ key_is_missing_or_empty = api_key is None or (isinstance(api_key, str) and not api_key.strip())
312
+
313
+ if api_key_required and key_is_missing_or_empty:
314
+ # Key is missing/empty after config resolution and fallback generation.
315
+ # Re-check environment variables as a final step before erroring.
316
+ specific_env_var_name = f"{target_llm_name.upper()}_API_KEY"
317
+ common_fallback_var = "OPENAI_API_KEY"
318
+ specific_key_from_env = os.getenv(specific_env_var_name)
319
+ fallback_key_from_env = os.getenv(common_fallback_var)
320
+
321
+ if specific_key_from_env:
322
+ logger.info(f"API key missing/empty in config/fallback for '{target_llm_name}', using env var '{specific_env_var_name}'.")
323
+ llm_config["api_key"] = specific_key_from_env # Update config with key from env
324
+ elif fallback_key_from_env:
325
+ logger.info(f"API key missing/empty for '{target_llm_name}' and specific env var '{specific_env_var_name}' not set. Using fallback env var '{common_fallback_var}'.")
326
+ llm_config["api_key"] = fallback_key_from_env # Update config with key from env
327
+ else:
328
+ # If still missing after checking env vars again, raise error
329
+ raise ValueError(f"Required API key for LLM profile '{target_llm_name}' is missing or empty. Checked config, fallback generation, env var '{specific_env_var_name}', and fallback '{common_fallback_var}'.")
330
+
331
+ # Log final config being used
332
+ if logger.isEnabledFor(logging.DEBUG): logger.debug(f"Final loaded config for '{target_llm_name}': {redact_sensitive_data(llm_config)}")
333
+ return llm_config
334
+
178
335
 
179
336
  def get_llm_model(config_dict: Dict[str, Any], llm_name: Optional[str] = None) -> str:
337
+ """Retrieves the 'model' name string for a specific LLM profile."""
180
338
  target_llm_name = llm_name or os.getenv("DEFAULT_LLM", "default")
181
339
  try: llm_config = load_llm_config(config_dict, target_llm_name)
182
340
  except ValueError as e: raise ValueError(f"Could not load config for LLM '{target_llm_name}': {e}") from e
@@ -186,23 +344,9 @@ def get_llm_model(config_dict: Dict[str, Any], llm_name: Optional[str] = None) -
186
344
  return model_name
187
345
 
188
346
  def load_and_validate_llm(config_dict: Dict[str, Any], llm_name: Optional[str] = None) -> Dict[str, Any]:
347
+ """Loads and validates config for a specific LLM (wrapper for load_llm_config)."""
189
348
  target_llm_name = llm_name or os.getenv("DEFAULT_LLM", "default")
190
349
  logger.debug(f"Loading and validating LLM (via load_llm_config) for profile: {target_llm_name}")
191
350
  return load_llm_config(config_dict, target_llm_name)
192
351
 
193
- def get_server_params(server_config: Dict[str, Any], server_name: str) -> Optional[Dict[str, Any]]:
194
- """Extracts and validates parameters needed to start an MCP server."""
195
- command = server_config.get("command"); args = server_config.get("args", []); config_env = server_config.get("env", {})
196
- if not command: logger.error(f"MCP server '{server_name}' missing 'command'."); return None
197
- if not isinstance(args, list): logger.error(f"MCP server '{server_name}' 'args' must be list."); return None
198
- if not isinstance(config_env, dict): logger.error(f"MCP server '{server_name}' 'env' must be dict."); return None
199
- env = {**os.environ.copy(), **config_env}; valid_env = {}
200
- for k, v in env.items():
201
- if v is None: logger.warning(f"Env var '{k}' for MCP server '{server_name}' resolved to None. Omitting.")
202
- else: valid_env[k] = str(v)
203
- return {"command": command, "args": args, "env": valid_env}
204
-
205
- def list_mcp_servers(config_dict: Dict[str, Any]) -> List[str]:
206
- """Returns a list of configured MCP server names."""
207
- return list(config_dict.get("mcpServers", {}).keys())
208
-
352
+ # --- End of Missing Functions ---
@@ -0,0 +1 @@
1
+ # This is the __init__.py for the 'mcp' package.
@@ -0,0 +1,32 @@
1
+ # cache_utils.py
2
+
3
+ from typing import Any
4
+
5
+ class DummyCache:
6
+ """A dummy cache that performs no operations."""
7
+ def get(self, key: str, default: Any = None) -> Any:
8
+ return default
9
+
10
+ def set(self, key: str, value: Any, timeout: int = None) -> None:
11
+ pass
12
+
13
+ def get_cache():
14
+ """
15
+ Attempts to retrieve Django's cache. If Django isn't available or configured,
16
+ returns a DummyCache instance.
17
+ """
18
+ try:
19
+ import django
20
+ from django.conf import settings
21
+ from django.core.cache import cache as django_cache
22
+ from django.core.exceptions import ImproperlyConfigured
23
+
24
+ if not settings.configured:
25
+ # Django settings are not configured; return DummyCache
26
+ return DummyCache()
27
+
28
+ return django_cache
29
+
30
+ except (ImportError, ImproperlyConfigured):
31
+ # Django is not installed or not properly configured; use DummyCache
32
+ return DummyCache()