open-swarm 0.1.1745275181__py3-none-any.whl → 0.1.1748636295__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. open_swarm-0.1.1748636295.dist-info/METADATA +257 -0
  2. open_swarm-0.1.1748636295.dist-info/RECORD +89 -0
  3. {open_swarm-0.1.1745275181.dist-info → open_swarm-0.1.1748636295.dist-info}/WHEEL +2 -1
  4. open_swarm-0.1.1748636295.dist-info/entry_points.txt +3 -0
  5. open_swarm-0.1.1748636295.dist-info/top_level.txt +1 -0
  6. swarm/__init__.py +2 -0
  7. swarm/agent/agent.py +49 -0
  8. swarm/auth.py +48 -113
  9. swarm/consumers.py +0 -19
  10. swarm/core.py +411 -0
  11. swarm/extensions/blueprint/__init__.py +16 -30
  12. swarm/extensions/blueprint/agent_utils.py +45 -0
  13. swarm/extensions/blueprint/blueprint_base.py +562 -0
  14. swarm/extensions/blueprint/blueprint_discovery.py +112 -0
  15. swarm/extensions/blueprint/django_utils.py +79 -181
  16. swarm/extensions/blueprint/interactive_mode.py +72 -67
  17. swarm/extensions/blueprint/output_utils.py +82 -0
  18. swarm/{core → extensions/blueprint}/spinner.py +21 -30
  19. swarm/extensions/cli/cli_args.py +0 -6
  20. swarm/extensions/cli/commands/blueprint_management.py +9 -47
  21. swarm/extensions/cli/commands/config_management.py +6 -5
  22. swarm/extensions/cli/commands/edit_config.py +7 -16
  23. swarm/extensions/cli/commands/list_blueprints.py +1 -1
  24. swarm/extensions/cli/commands/validate_env.py +4 -11
  25. swarm/extensions/cli/commands/validate_envvars.py +6 -6
  26. swarm/extensions/cli/interactive_shell.py +2 -16
  27. swarm/extensions/config/config_loader.py +345 -107
  28. swarm/{core → extensions/config}/config_manager.py +38 -50
  29. swarm/{core → extensions/config}/server_config.py +0 -32
  30. swarm/extensions/launchers/build_launchers.py +14 -0
  31. swarm/{core → extensions/launchers}/build_swarm_wrapper.py +0 -0
  32. swarm/extensions/launchers/swarm_api.py +64 -8
  33. swarm/extensions/launchers/swarm_cli.py +300 -8
  34. swarm/extensions/mcp/__init__.py +1 -0
  35. swarm/extensions/mcp/cache_utils.py +32 -0
  36. swarm/extensions/mcp/mcp_client.py +233 -0
  37. swarm/extensions/mcp/mcp_tool_provider.py +135 -0
  38. swarm/extensions/mcp/mcp_utils.py +260 -0
  39. swarm/llm/chat_completion.py +166 -0
  40. swarm/serializers.py +5 -96
  41. swarm/settings.py +133 -85
  42. swarm/types.py +91 -0
  43. swarm/urls.py +74 -57
  44. swarm/utils/context_utils.py +4 -10
  45. swarm/utils/general_utils.py +0 -21
  46. swarm/utils/redact.py +36 -23
  47. swarm/views/api_views.py +39 -48
  48. swarm/views/chat_views.py +76 -236
  49. swarm/views/core_views.py +87 -80
  50. swarm/views/model_views.py +121 -64
  51. swarm/views/utils.py +439 -65
  52. swarm/views/web_views.py +2 -2
  53. open_swarm-0.1.1745275181.dist-info/METADATA +0 -874
  54. open_swarm-0.1.1745275181.dist-info/RECORD +0 -319
  55. open_swarm-0.1.1745275181.dist-info/entry_points.txt +0 -4
  56. swarm/blueprints/README.md +0 -68
  57. swarm/blueprints/blueprint_audit_status.json +0 -27
  58. swarm/blueprints/chatbot/README.md +0 -40
  59. swarm/blueprints/chatbot/blueprint_chatbot.py +0 -471
  60. swarm/blueprints/chatbot/metadata.json +0 -23
  61. swarm/blueprints/chatbot/templates/chatbot/chatbot.html +0 -33
  62. swarm/blueprints/chucks_angels/README.md +0 -11
  63. swarm/blueprints/chucks_angels/blueprint_chucks_angels.py +0 -7
  64. swarm/blueprints/chucks_angels/test_basic.py +0 -3
  65. swarm/blueprints/codey/CODEY.md +0 -15
  66. swarm/blueprints/codey/README.md +0 -115
  67. swarm/blueprints/codey/blueprint_codey.py +0 -1072
  68. swarm/blueprints/codey/codey_cli.py +0 -373
  69. swarm/blueprints/codey/instructions.md +0 -17
  70. swarm/blueprints/codey/metadata.json +0 -23
  71. swarm/blueprints/common/operation_box_utils.py +0 -83
  72. swarm/blueprints/digitalbutlers/README.md +0 -11
  73. swarm/blueprints/digitalbutlers/__init__.py +0 -1
  74. swarm/blueprints/digitalbutlers/blueprint_digitalbutlers.py +0 -7
  75. swarm/blueprints/digitalbutlers/test_basic.py +0 -3
  76. swarm/blueprints/divine_code/README.md +0 -3
  77. swarm/blueprints/divine_code/__init__.py +0 -10
  78. swarm/blueprints/divine_code/apps.py +0 -11
  79. swarm/blueprints/divine_code/blueprint_divine_code.py +0 -270
  80. swarm/blueprints/django_chat/apps.py +0 -6
  81. swarm/blueprints/django_chat/blueprint_django_chat.py +0 -268
  82. swarm/blueprints/django_chat/templates/django_chat/django_chat_webpage.html +0 -37
  83. swarm/blueprints/django_chat/urls.py +0 -8
  84. swarm/blueprints/django_chat/views.py +0 -32
  85. swarm/blueprints/echocraft/blueprint_echocraft.py +0 -384
  86. swarm/blueprints/flock/README.md +0 -11
  87. swarm/blueprints/flock/__init__.py +0 -8
  88. swarm/blueprints/flock/blueprint_flock.py +0 -7
  89. swarm/blueprints/flock/test_basic.py +0 -3
  90. swarm/blueprints/geese/README.md +0 -10
  91. swarm/blueprints/geese/__init__.py +0 -8
  92. swarm/blueprints/geese/blueprint_geese.py +0 -384
  93. swarm/blueprints/geese/geese_cli.py +0 -102
  94. swarm/blueprints/jeeves/README.md +0 -41
  95. swarm/blueprints/jeeves/blueprint_jeeves.py +0 -722
  96. swarm/blueprints/jeeves/jeeves_cli.py +0 -55
  97. swarm/blueprints/jeeves/metadata.json +0 -24
  98. swarm/blueprints/mcp_demo/blueprint_mcp_demo.py +0 -473
  99. swarm/blueprints/messenger/templates/messenger/messenger.html +0 -46
  100. swarm/blueprints/mission_improbable/blueprint_mission_improbable.py +0 -423
  101. swarm/blueprints/monkai_magic/blueprint_monkai_magic.py +0 -340
  102. swarm/blueprints/nebula_shellz/blueprint_nebula_shellz.py +0 -265
  103. swarm/blueprints/omniplex/blueprint_omniplex.py +0 -298
  104. swarm/blueprints/poets/blueprint_poets.py +0 -546
  105. swarm/blueprints/poets/poets_cli.py +0 -23
  106. swarm/blueprints/rue_code/README.md +0 -8
  107. swarm/blueprints/rue_code/blueprint_rue_code.py +0 -448
  108. swarm/blueprints/rue_code/rue_code_cli.py +0 -43
  109. swarm/blueprints/stewie/apps.py +0 -12
  110. swarm/blueprints/stewie/blueprint_family_ties.py +0 -349
  111. swarm/blueprints/stewie/models.py +0 -19
  112. swarm/blueprints/stewie/serializers.py +0 -10
  113. swarm/blueprints/stewie/settings.py +0 -17
  114. swarm/blueprints/stewie/urls.py +0 -11
  115. swarm/blueprints/stewie/views.py +0 -26
  116. swarm/blueprints/suggestion/blueprint_suggestion.py +0 -222
  117. swarm/blueprints/whinge_surf/README.md +0 -22
  118. swarm/blueprints/whinge_surf/__init__.py +0 -1
  119. swarm/blueprints/whinge_surf/blueprint_whinge_surf.py +0 -565
  120. swarm/blueprints/whinge_surf/whinge_surf_cli.py +0 -99
  121. swarm/blueprints/whiskeytango_foxtrot/__init__.py +0 -0
  122. swarm/blueprints/whiskeytango_foxtrot/apps.py +0 -11
  123. swarm/blueprints/whiskeytango_foxtrot/blueprint_whiskeytango_foxtrot.py +0 -339
  124. swarm/blueprints/zeus/__init__.py +0 -2
  125. swarm/blueprints/zeus/apps.py +0 -4
  126. swarm/blueprints/zeus/blueprint_zeus.py +0 -270
  127. swarm/blueprints/zeus/zeus_cli.py +0 -13
  128. swarm/cli/async_input.py +0 -65
  129. swarm/cli/async_input_demo.py +0 -32
  130. swarm/core/agent_utils.py +0 -21
  131. swarm/core/blueprint_base.py +0 -769
  132. swarm/core/blueprint_discovery.py +0 -125
  133. swarm/core/blueprint_runner.py +0 -59
  134. swarm/core/blueprint_ux.py +0 -109
  135. swarm/core/build_launchers.py +0 -15
  136. swarm/core/cli/__init__.py +0 -1
  137. swarm/core/cli/commands/__init__.py +0 -1
  138. swarm/core/cli/commands/blueprint_management.py +0 -7
  139. swarm/core/cli/interactive_shell.py +0 -14
  140. swarm/core/cli/main.py +0 -50
  141. swarm/core/cli/utils/__init__.py +0 -1
  142. swarm/core/cli/utils/discover_commands.py +0 -18
  143. swarm/core/config_loader.py +0 -122
  144. swarm/core/output_utils.py +0 -193
  145. swarm/core/session_logger.py +0 -42
  146. swarm/core/slash_commands.py +0 -89
  147. swarm/core/swarm_api.py +0 -68
  148. swarm/core/swarm_cli.py +0 -216
  149. swarm/core/utils/__init__.py +0 -0
  150. swarm/extensions/blueprint/cli_handler.py +0 -197
  151. swarm/extensions/blueprint/runnable_blueprint.py +0 -42
  152. swarm/extensions/cli/utils/__init__.py +0 -1
  153. swarm/extensions/cli/utils/async_input.py +0 -46
  154. swarm/extensions/cli/utils/prompt_user.py +0 -3
  155. swarm/management/__init__.py +0 -0
  156. swarm/management/commands/__init__.py +0 -0
  157. swarm/management/commands/runserver.py +0 -58
  158. swarm/middleware.py +0 -65
  159. swarm/permissions.py +0 -38
  160. swarm/static/contrib/fonts/fontawesome-webfont.ttf +0 -7
  161. swarm/static/contrib/fonts/fontawesome-webfont.woff +0 -7
  162. swarm/static/contrib/fonts/fontawesome-webfont.woff2 +0 -7
  163. swarm/static/contrib/markedjs/marked.min.js +0 -6
  164. swarm/static/contrib/tabler-icons/adjustments-horizontal.svg +0 -27
  165. swarm/static/contrib/tabler-icons/alert-triangle.svg +0 -21
  166. swarm/static/contrib/tabler-icons/archive.svg +0 -21
  167. swarm/static/contrib/tabler-icons/artboard.svg +0 -27
  168. swarm/static/contrib/tabler-icons/automatic-gearbox.svg +0 -23
  169. swarm/static/contrib/tabler-icons/box-multiple.svg +0 -19
  170. swarm/static/contrib/tabler-icons/carambola.svg +0 -19
  171. swarm/static/contrib/tabler-icons/copy.svg +0 -20
  172. swarm/static/contrib/tabler-icons/download.svg +0 -21
  173. swarm/static/contrib/tabler-icons/edit.svg +0 -21
  174. swarm/static/contrib/tabler-icons/filled/carambola.svg +0 -13
  175. swarm/static/contrib/tabler-icons/filled/paint.svg +0 -13
  176. swarm/static/contrib/tabler-icons/headset.svg +0 -22
  177. swarm/static/contrib/tabler-icons/layout-sidebar-left-collapse.svg +0 -21
  178. swarm/static/contrib/tabler-icons/layout-sidebar-left-expand.svg +0 -21
  179. swarm/static/contrib/tabler-icons/layout-sidebar-right-collapse.svg +0 -21
  180. swarm/static/contrib/tabler-icons/layout-sidebar-right-expand.svg +0 -21
  181. swarm/static/contrib/tabler-icons/message-chatbot.svg +0 -22
  182. swarm/static/contrib/tabler-icons/message-star.svg +0 -22
  183. swarm/static/contrib/tabler-icons/message-x.svg +0 -23
  184. swarm/static/contrib/tabler-icons/message.svg +0 -21
  185. swarm/static/contrib/tabler-icons/paperclip.svg +0 -18
  186. swarm/static/contrib/tabler-icons/playlist-add.svg +0 -22
  187. swarm/static/contrib/tabler-icons/robot.svg +0 -26
  188. swarm/static/contrib/tabler-icons/search.svg +0 -19
  189. swarm/static/contrib/tabler-icons/settings.svg +0 -20
  190. swarm/static/contrib/tabler-icons/thumb-down.svg +0 -19
  191. swarm/static/contrib/tabler-icons/thumb-up.svg +0 -19
  192. swarm/static/css/dropdown.css +0 -22
  193. swarm/static/htmx/htmx.min.js +0 -0
  194. swarm/static/js/dropdown.js +0 -23
  195. swarm/static/rest_mode/css/base.css +0 -470
  196. swarm/static/rest_mode/css/chat-history.css +0 -286
  197. swarm/static/rest_mode/css/chat.css +0 -251
  198. swarm/static/rest_mode/css/chatbot.css +0 -74
  199. swarm/static/rest_mode/css/chatgpt.css +0 -62
  200. swarm/static/rest_mode/css/colors/corporate.css +0 -74
  201. swarm/static/rest_mode/css/colors/pastel.css +0 -81
  202. swarm/static/rest_mode/css/colors/tropical.css +0 -82
  203. swarm/static/rest_mode/css/general.css +0 -142
  204. swarm/static/rest_mode/css/layout.css +0 -167
  205. swarm/static/rest_mode/css/layouts/messenger-layout.css +0 -17
  206. swarm/static/rest_mode/css/layouts/minimalist-layout.css +0 -57
  207. swarm/static/rest_mode/css/layouts/mobile-layout.css +0 -8
  208. swarm/static/rest_mode/css/messages.css +0 -84
  209. swarm/static/rest_mode/css/messenger.css +0 -135
  210. swarm/static/rest_mode/css/settings.css +0 -91
  211. swarm/static/rest_mode/css/simple.css +0 -44
  212. swarm/static/rest_mode/css/slack.css +0 -58
  213. swarm/static/rest_mode/css/style.css +0 -156
  214. swarm/static/rest_mode/css/theme.css +0 -30
  215. swarm/static/rest_mode/css/toast.css +0 -40
  216. swarm/static/rest_mode/js/auth.js +0 -9
  217. swarm/static/rest_mode/js/blueprint.js +0 -41
  218. swarm/static/rest_mode/js/blueprintUtils.js +0 -12
  219. swarm/static/rest_mode/js/chatLogic.js +0 -79
  220. swarm/static/rest_mode/js/debug.js +0 -63
  221. swarm/static/rest_mode/js/events.js +0 -98
  222. swarm/static/rest_mode/js/main.js +0 -19
  223. swarm/static/rest_mode/js/messages.js +0 -264
  224. swarm/static/rest_mode/js/messengerLogic.js +0 -355
  225. swarm/static/rest_mode/js/modules/apiService.js +0 -84
  226. swarm/static/rest_mode/js/modules/blueprintManager.js +0 -162
  227. swarm/static/rest_mode/js/modules/chatHistory.js +0 -110
  228. swarm/static/rest_mode/js/modules/debugLogger.js +0 -14
  229. swarm/static/rest_mode/js/modules/eventHandlers.js +0 -107
  230. swarm/static/rest_mode/js/modules/messageProcessor.js +0 -120
  231. swarm/static/rest_mode/js/modules/state.js +0 -7
  232. swarm/static/rest_mode/js/modules/userInteractions.js +0 -29
  233. swarm/static/rest_mode/js/modules/validation.js +0 -23
  234. swarm/static/rest_mode/js/rendering.js +0 -119
  235. swarm/static/rest_mode/js/settings.js +0 -130
  236. swarm/static/rest_mode/js/sidebar.js +0 -94
  237. swarm/static/rest_mode/js/simpleLogic.js +0 -37
  238. swarm/static/rest_mode/js/slackLogic.js +0 -66
  239. swarm/static/rest_mode/js/splash.js +0 -76
  240. swarm/static/rest_mode/js/theme.js +0 -111
  241. swarm/static/rest_mode/js/toast.js +0 -36
  242. swarm/static/rest_mode/js/ui.js +0 -265
  243. swarm/static/rest_mode/js/validation.js +0 -57
  244. swarm/static/rest_mode/svg/animated_spinner.svg +0 -12
  245. swarm/static/rest_mode/svg/arrow_down.svg +0 -5
  246. swarm/static/rest_mode/svg/arrow_left.svg +0 -5
  247. swarm/static/rest_mode/svg/arrow_right.svg +0 -5
  248. swarm/static/rest_mode/svg/arrow_up.svg +0 -5
  249. swarm/static/rest_mode/svg/attach.svg +0 -8
  250. swarm/static/rest_mode/svg/avatar.svg +0 -7
  251. swarm/static/rest_mode/svg/canvas.svg +0 -6
  252. swarm/static/rest_mode/svg/chat_history.svg +0 -4
  253. swarm/static/rest_mode/svg/close.svg +0 -5
  254. swarm/static/rest_mode/svg/copy.svg +0 -4
  255. swarm/static/rest_mode/svg/dark_mode.svg +0 -3
  256. swarm/static/rest_mode/svg/edit.svg +0 -5
  257. swarm/static/rest_mode/svg/layout.svg +0 -9
  258. swarm/static/rest_mode/svg/logo.svg +0 -29
  259. swarm/static/rest_mode/svg/logout.svg +0 -5
  260. swarm/static/rest_mode/svg/mobile.svg +0 -5
  261. swarm/static/rest_mode/svg/new_chat.svg +0 -4
  262. swarm/static/rest_mode/svg/not_visible.svg +0 -5
  263. swarm/static/rest_mode/svg/plus.svg +0 -7
  264. swarm/static/rest_mode/svg/run_code.svg +0 -6
  265. swarm/static/rest_mode/svg/save.svg +0 -4
  266. swarm/static/rest_mode/svg/search.svg +0 -6
  267. swarm/static/rest_mode/svg/settings.svg +0 -4
  268. swarm/static/rest_mode/svg/speaker.svg +0 -5
  269. swarm/static/rest_mode/svg/stop.svg +0 -6
  270. swarm/static/rest_mode/svg/thumbs_down.svg +0 -3
  271. swarm/static/rest_mode/svg/thumbs_up.svg +0 -3
  272. swarm/static/rest_mode/svg/toggle_off.svg +0 -6
  273. swarm/static/rest_mode/svg/toggle_on.svg +0 -6
  274. swarm/static/rest_mode/svg/trash.svg +0 -10
  275. swarm/static/rest_mode/svg/undo.svg +0 -3
  276. swarm/static/rest_mode/svg/visible.svg +0 -8
  277. swarm/static/rest_mode/svg/voice.svg +0 -10
  278. swarm/templates/account/login.html +0 -22
  279. swarm/templates/account/signup.html +0 -32
  280. swarm/templates/base.html +0 -30
  281. swarm/templates/chat.html +0 -43
  282. swarm/templates/index.html +0 -35
  283. swarm/templates/rest_mode/components/chat_sidebar.html +0 -55
  284. swarm/templates/rest_mode/components/header.html +0 -45
  285. swarm/templates/rest_mode/components/main_chat_pane.html +0 -41
  286. swarm/templates/rest_mode/components/settings_dialog.html +0 -97
  287. swarm/templates/rest_mode/components/splash_screen.html +0 -7
  288. swarm/templates/rest_mode/components/top_bar.html +0 -28
  289. swarm/templates/rest_mode/message_ui.html +0 -50
  290. swarm/templates/rest_mode/slackbot.html +0 -30
  291. swarm/templates/simple_blueprint_page.html +0 -24
  292. swarm/templates/websocket_partials/final_system_message.html +0 -3
  293. swarm/templates/websocket_partials/system_message.html +0 -4
  294. swarm/templates/websocket_partials/user_message.html +0 -5
  295. swarm/utils/ansi_box.py +0 -34
  296. swarm/utils/disable_tracing.py +0 -38
  297. swarm/utils/log_utils.py +0 -63
  298. swarm/utils/openai_patch.py +0 -33
  299. swarm/ux/ansi_box.py +0 -43
  300. swarm/ux/spinner.py +0 -53
  301. {open_swarm-0.1.1745275181.dist-info → open_swarm-0.1.1748636295.dist-info}/licenses/LICENSE +0 -0
  302. /swarm/{core → extensions/blueprint}/blueprint_utils.py +0 -0
  303. /swarm/{core → extensions/blueprint}/common_utils.py +0 -0
  304. /swarm/{core → extensions/config}/setup_wizard.py +0 -0
  305. /swarm/{blueprints/rue_code → extensions/config/utils}/__init__.py +0 -0
  306. /swarm/{core → extensions/config}/utils/logger.py +0 -0
  307. /swarm/{core → extensions/launchers}/swarm_wrapper.py +0 -0
@@ -1,114 +1,352 @@
1
- import json
1
+ """
2
+ Configuration Loader for Open Swarm MCP Framework.
3
+ """
4
+
2
5
  import os
3
- from pathlib import Path
6
+ import json
7
+ import re
4
8
  import logging
5
- from typing import Dict, Any, Optional
9
+ from typing import Any, Dict, List, Tuple, Optional
10
+ from pathlib import Path
11
+ from dotenv import load_dotenv
12
+ # Import save_server_config carefully
13
+ try: from .server_config import save_server_config
14
+ except ImportError: save_server_config = None
15
+ from swarm.settings import DEBUG, BASE_DIR
16
+ from swarm.utils.redact import redact_sensitive_data
6
17
 
7
18
  logger = logging.getLogger(__name__)
19
+ logger.setLevel(logging.DEBUG if DEBUG else logging.INFO)
20
+ if not logger.handlers:
21
+ stream_handler = logging.StreamHandler()
22
+ formatter = logging.Formatter("[%(levelname)s] %(asctime)s - %(name)s:%(lineno)d - %(message)s")
23
+ stream_handler.setFormatter(formatter)
24
+ logger.addHandler(stream_handler)
25
+
26
+ config: Dict[str, Any] = {}
27
+ load_dotenv()
28
+ logger.debug("Environment variables potentially loaded from .env file.")
8
29
 
9
- DEFAULT_CONFIG_FILENAME = "swarm_config.json"
10
-
11
- # --- find_config_file, load_config, save_config, validate_config, get_profile_from_config, _substitute_env_vars_recursive ---
12
- # (Keep these functions as they were)
13
- def find_config_file( specific_path: Optional[str]=None, start_dir: Optional[Path]=None, default_dir: Optional[Path]=None,) -> Optional[Path]:
14
- # 1. XDG config path
15
- xdg_config = Path(os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config"))) / "swarm" / DEFAULT_CONFIG_FILENAME
16
- if xdg_config.is_file():
17
- logger.debug(f"Found config XDG: {xdg_config}")
18
- return xdg_config.resolve()
19
- # 2. User-specified path
20
- if specific_path:
21
- p = Path(specific_path)
22
- return p.resolve() if p.is_file() else logger.warning(f"Specified config path DNE: {specific_path}") or None # Fall through
23
- # 3. Upwards from start_dir
24
- if start_dir:
25
- current = start_dir.resolve()
26
- while current != current.parent:
27
- if (cp := current / DEFAULT_CONFIG_FILENAME).is_file():
28
- logger.debug(f"Found config upwards: {cp}")
29
- return cp.resolve()
30
- current = current.parent
31
- if (cp := current / DEFAULT_CONFIG_FILENAME).is_file():
32
- logger.debug(f"Found config at root: {cp}")
33
- return cp.resolve()
34
- # 4. Default dir
35
- if default_dir and (cp := default_dir.resolve() / DEFAULT_CONFIG_FILENAME).is_file():
36
- logger.debug(f"Found config default: {cp}")
37
- return cp.resolve()
38
- # 5. CWD
39
- cwd = Path.cwd()
40
- if start_dir is None or cwd != start_dir.resolve():
41
- if (cp := cwd / DEFAULT_CONFIG_FILENAME).is_file():
42
- logger.debug(f"Found config cwd: {cp}")
43
- return cp.resolve()
44
- logger.debug(f"Config '{DEFAULT_CONFIG_FILENAME}' not found.")
45
- return None
46
-
47
- def load_config(config_path: Path) -> Dict[str, Any]:
48
- logger.debug(f"Loading config from {config_path}")
30
+ def process_config(config_dict: dict) -> dict:
31
+ """Processes config: resolves placeholders, merges external MCP."""
49
32
  try:
50
- with open(config_path, 'r') as f: config = json.load(f)
51
- logger.info(f"Loaded config from {config_path}"); validate_config(config); return config
52
- except FileNotFoundError: logger.error(f"Config DNE: {config_path}"); raise
53
- except json.JSONDecodeError as e: logger.error(f"JSON error {config_path}: {e}"); raise ValueError(f"Invalid JSON: {config_path}") from e
54
- except Exception as e: logger.error(f"Load error {config_path}: {e}"); raise
55
-
56
- def save_config(config: Dict[str, Any], config_path: Path):
57
- logger.info(f"Saving config to {config_path}")
58
- try: config_path.parent.mkdir(parents=True,exist_ok=True); f = config_path.open('w'); json.dump(config, f, indent=4); f.close(); logger.debug("Save OK.")
59
- except Exception as e: logger.error(f"Save failed {config_path}: {e}", exc_info=True); raise
60
-
61
- def validate_config(config: Dict[str, Any]):
62
- logger.debug("Validating config structure...")
63
- if "llm" not in config or not isinstance(config["llm"],dict): raise ValueError("Config 'llm' section missing/malformed.")
64
- for name, prof in config.get("llm",{}).items():
65
- if not isinstance(prof,dict): raise ValueError(f"LLM profile '{name}' not dict.")
66
- logger.debug("Config basic structure OK.")
67
-
68
- def get_profile_from_config(config: Dict[str, Any], profile_name: str) -> Dict[str, Any]:
69
- profile_data = config.get("llm", {}).get(profile_name)
70
- if profile_data is None: raise ValueError(f"LLM profile '{profile_name}' not found.")
71
- if not isinstance(profile_data, dict): raise ValueError(f"LLM profile '{profile_name}' not dict.")
72
- return _substitute_env_vars_recursive(profile_data)
73
-
74
- def _substitute_env_vars_recursive(data: Any) -> Any:
75
- if isinstance(data,dict): return {k:_substitute_env_vars_recursive(v) for k,v in data.items()}
76
- if isinstance(data,list): return [_substitute_env_vars_recursive(i) for i in data]
77
- if isinstance(data,str): return os.path.expandvars(data)
78
- return data
79
-
80
- def _substitute_env_vars(data: Any) -> Any:
81
- """Public API: Recursively substitute environment variables in dict, list, str."""
82
- return _substitute_env_vars_recursive(data)
83
-
84
- def create_default_config(config_path: Path):
85
- """Creates a default configuration file with valid JSON."""
86
- default_config = {
87
- "llm": {
88
- "default": {
89
- "provider": "openai",
90
- "model": "gpt-4o",
91
- "api_key": "${OPENAI_API_KEY}",
92
- "base_url": None,
93
- "description": "Default OpenAI profile. Requires OPENAI_API_KEY env var."
94
- },
95
- "ollama_example": {
96
- "provider": "ollama",
97
- "model": "llama3",
98
- "api_key": "ollama", # Usually not needed
99
- "base_url": "http://localhost:11434",
100
- "description": "Example for local Ollama Llama 3 model."
101
- }
102
- },
103
- "agents": {},
104
- "settings": {
105
- "default_markdown_output": True
106
- }
107
- }
108
- logger.info(f"Creating default configuration file at {config_path}")
33
+ resolved_config = resolve_placeholders(config_dict)
34
+ if logger.isEnabledFor(logging.DEBUG): logger.debug("Config after resolving placeholders: " + json.dumps(redact_sensitive_data(resolved_config), indent=2))
35
+
36
+ disable_merge = os.getenv("DISABLE_MCP_MERGE", "false").lower() in ("true", "1", "yes")
37
+ if not disable_merge:
38
+ if os.name == "nt": external_mcp_path = Path(os.getenv("APPDATA", Path.home())) / "Claude" / "claude_desktop_config.json"
39
+ else: external_mcp_path = Path.home() / ".vscode-server" / "data" / "User" / "globalStorage" / "rooveterinaryinc.roo-cline" / "settings" / "cline_mcp_settings.json"
40
+
41
+ if external_mcp_path.exists():
42
+ logger.info(f"Found external MCP settings file at: {external_mcp_path}")
43
+ try:
44
+ with open(external_mcp_path, "r") as mcp_file: external_mcp_config = json.load(mcp_file)
45
+ if logger.isEnabledFor(logging.DEBUG): logger.debug("Loaded external MCP settings: " + json.dumps(redact_sensitive_data(external_mcp_config), indent=2))
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))
59
+ else: logger.debug("No new MCP servers added from external settings.")
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))
61
+ else: logger.debug(f"External MCP settings file not found at {external_mcp_path}. Skipping merge.")
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
64
+ globals()["config"] = resolved_config
65
+ return resolved_config
66
+
67
+ def resolve_placeholders(obj: Any) -> Any:
68
+ """Recursively resolve ${VAR_NAME} placeholders. Returns None if var not found."""
69
+ if isinstance(obj, dict): return {k: resolve_placeholders(v) for k, v in obj.items()}
70
+ elif isinstance(obj, list): return [resolve_placeholders(item) for item in obj]
71
+ elif isinstance(obj, str):
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}}}'
79
+ if env_value is None:
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
82
+ if resolved_string == placeholder:
83
+ return None
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
88
+ else:
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
+
97
+ return resolved_string
98
+ else: return obj
99
+
100
+ def load_server_config(file_path: Optional[str] = None) -> dict:
101
+ """Loads, resolves, and merges server config from JSON file."""
102
+ config_path: Optional[Path] = None
103
+ if file_path:
104
+ path_obj = Path(file_path)
105
+ if path_obj.is_file(): config_path = path_obj; logger.info(f"Using provided config file path: {config_path}")
106
+ else: logger.warning(f"Provided path '{file_path}' not found/not file. Searching standard locations.")
107
+ if not 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}")
114
+ try:
115
+ # Ensure reading with UTF-8 encoding
116
+ raw_config = json.loads(config_path.read_text(encoding='utf-8'))
117
+ if logger.isEnabledFor(logging.DEBUG): logger.debug(f"Raw config loaded: {redact_sensitive_data(raw_config)}")
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.")
109
249
  try:
110
- save_config(default_config, config_path) # Use save_config to write valid JSON
111
- logger.debug("Default configuration file created successfully.")
112
- except Exception as e:
113
- logger.error(f"Failed to create default config file at {config_path}: {e}", exc_info=True)
114
- raise
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)
260
+
261
+ def load_llm_config(config_dict: Optional[Dict[str, Any]] = None, llm_name: Optional[str] = None) -> Dict[str, Any]:
262
+ """Loads and validates config for a specific LLM profile."""
263
+ if config_dict is None:
264
+ # Try loading from global if not provided
265
+ global_config = globals().get("config")
266
+ if not global_config:
267
+ try: config_dict = load_server_config(); globals()["config"] = config_dict
268
+ except Exception as e: raise ValueError("Global config not loaded and no config_dict provided.") from e
269
+ else:
270
+ config_dict = global_config
271
+
272
+ target_llm_name = llm_name or os.getenv("DEFAULT_LLM", "default")
273
+ logger.debug(f"Loading LLM config for profile: '{target_llm_name}'")
274
+ # Resolve placeholders FIRST using the provided or loaded config_dict
275
+ resolved_config = resolve_placeholders(config_dict)
276
+
277
+ llm_profiles = resolved_config.get("llm", {})
278
+ if not isinstance(llm_profiles, dict): raise ValueError("'llm' section must be a dictionary.")
279
+ llm_config = llm_profiles.get(target_llm_name)
280
+
281
+ # Fallback Logic (if profile not found after resolving)
282
+ if not llm_config:
283
+ logger.warning(f"LLM config for '{target_llm_name}' not found. Generating fallback.")
284
+ fb_provider = os.getenv("DEFAULT_LLM_PROVIDER", "openai"); fb_model = os.getenv("DEFAULT_LLM_MODEL", "gpt-4o")
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)}")
304
+
305
+ if not isinstance(llm_config, dict): raise ValueError(f"LLM profile '{target_llm_name}' must be a dictionary.")
306
+
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
+
335
+
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."""
338
+ target_llm_name = llm_name or os.getenv("DEFAULT_LLM", "default")
339
+ try: llm_config = load_llm_config(config_dict, target_llm_name)
340
+ except ValueError as e: raise ValueError(f"Could not load config for LLM '{target_llm_name}': {e}") from e
341
+ model_name = llm_config.get("model")
342
+ if not model_name or not isinstance(model_name, str): raise ValueError(f"'model' name missing/invalid for LLM '{target_llm_name}'.")
343
+ logger.debug(f"Retrieved model name '{model_name}' for LLM '{target_llm_name}'")
344
+ return model_name
345
+
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)."""
348
+ target_llm_name = llm_name or os.getenv("DEFAULT_LLM", "default")
349
+ logger.debug(f"Loading and validating LLM (via load_llm_config) for profile: {target_llm_name}")
350
+ return load_llm_config(config_dict, target_llm_name)
351
+
352
+ # --- End of Missing Functions ---