crca 1.4.0__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 (501) hide show
  1. .github/ISSUE_TEMPLATE/bug_report.md +65 -0
  2. .github/ISSUE_TEMPLATE/feature_request.md +41 -0
  3. .github/PULL_REQUEST_TEMPLATE.md +20 -0
  4. .github/workflows/publish-manual.yml +61 -0
  5. .github/workflows/publish.yml +64 -0
  6. .gitignore +214 -0
  7. CRCA.py +4156 -0
  8. LICENSE +201 -0
  9. MANIFEST.in +43 -0
  10. PKG-INFO +5035 -0
  11. README.md +4959 -0
  12. __init__.py +17 -0
  13. branches/CRCA-Q.py +2728 -0
  14. branches/crca_cg/corposwarm.py +9065 -0
  15. branches/crca_cg/fix_rancher_docker_creds.ps1 +155 -0
  16. branches/crca_cg/package.json +5 -0
  17. branches/crca_cg/test_bolt_integration.py +446 -0
  18. branches/crca_cg/test_corposwarm_comprehensive.py +773 -0
  19. branches/crca_cg/test_new_features.py +163 -0
  20. branches/crca_sd/__init__.py +149 -0
  21. branches/crca_sd/crca_sd_core.py +770 -0
  22. branches/crca_sd/crca_sd_governance.py +1325 -0
  23. branches/crca_sd/crca_sd_mpc.py +1130 -0
  24. branches/crca_sd/crca_sd_realtime.py +1844 -0
  25. branches/crca_sd/crca_sd_tui.py +1133 -0
  26. crca-1.4.0.dist-info/METADATA +5035 -0
  27. crca-1.4.0.dist-info/RECORD +501 -0
  28. crca-1.4.0.dist-info/WHEEL +4 -0
  29. crca-1.4.0.dist-info/licenses/LICENSE +201 -0
  30. docs/CRCA-Q.md +2333 -0
  31. examples/config.yaml.example +25 -0
  32. examples/crca_sd_example.py +513 -0
  33. examples/data_broker_example.py +294 -0
  34. examples/logistics_corporation.py +861 -0
  35. examples/palantir_example.py +299 -0
  36. examples/policy_bench.py +934 -0
  37. examples/pridnestrovia-sd.py +705 -0
  38. examples/pridnestrovia_realtime.py +1902 -0
  39. prompts/__init__.py +10 -0
  40. prompts/default_crca.py +101 -0
  41. pyproject.toml +151 -0
  42. requirements.txt +76 -0
  43. schemas/__init__.py +43 -0
  44. schemas/mcpSchemas.py +51 -0
  45. schemas/policy.py +458 -0
  46. templates/__init__.py +38 -0
  47. templates/base_specialized_agent.py +195 -0
  48. templates/drift_detection.py +325 -0
  49. templates/examples/causal_agent_template.py +309 -0
  50. templates/examples/drag_drop_example.py +213 -0
  51. templates/examples/logistics_agent_template.py +207 -0
  52. templates/examples/trading_agent_template.py +206 -0
  53. templates/feature_mixins.py +253 -0
  54. templates/graph_management.py +442 -0
  55. templates/llm_integration.py +194 -0
  56. templates/module_registry.py +276 -0
  57. templates/mpc_planner.py +280 -0
  58. templates/policy_loop.py +1168 -0
  59. templates/prediction_framework.py +448 -0
  60. templates/statistical_methods.py +778 -0
  61. tests/sanity.yml +31 -0
  62. tests/sanity_check +406 -0
  63. tests/test_core.py +47 -0
  64. tests/test_crca_excel.py +166 -0
  65. tests/test_crca_sd.py +780 -0
  66. tests/test_data_broker.py +424 -0
  67. tests/test_palantir.py +349 -0
  68. tools/__init__.py +38 -0
  69. tools/actuators.py +437 -0
  70. tools/bolt.diy/Dockerfile +103 -0
  71. tools/bolt.diy/app/components/@settings/core/AvatarDropdown.tsx +175 -0
  72. tools/bolt.diy/app/components/@settings/core/ControlPanel.tsx +345 -0
  73. tools/bolt.diy/app/components/@settings/core/constants.tsx +108 -0
  74. tools/bolt.diy/app/components/@settings/core/types.ts +114 -0
  75. tools/bolt.diy/app/components/@settings/index.ts +12 -0
  76. tools/bolt.diy/app/components/@settings/shared/components/TabTile.tsx +151 -0
  77. tools/bolt.diy/app/components/@settings/shared/service-integration/ConnectionForm.tsx +193 -0
  78. tools/bolt.diy/app/components/@settings/shared/service-integration/ConnectionTestIndicator.tsx +60 -0
  79. tools/bolt.diy/app/components/@settings/shared/service-integration/ErrorState.tsx +102 -0
  80. tools/bolt.diy/app/components/@settings/shared/service-integration/LoadingState.tsx +94 -0
  81. tools/bolt.diy/app/components/@settings/shared/service-integration/ServiceHeader.tsx +72 -0
  82. tools/bolt.diy/app/components/@settings/shared/service-integration/index.ts +6 -0
  83. tools/bolt.diy/app/components/@settings/tabs/data/DataTab.tsx +721 -0
  84. tools/bolt.diy/app/components/@settings/tabs/data/DataVisualization.tsx +384 -0
  85. tools/bolt.diy/app/components/@settings/tabs/event-logs/EventLogsTab.tsx +1013 -0
  86. tools/bolt.diy/app/components/@settings/tabs/features/FeaturesTab.tsx +295 -0
  87. tools/bolt.diy/app/components/@settings/tabs/github/GitHubTab.tsx +281 -0
  88. tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubAuthDialog.tsx +173 -0
  89. tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubCacheManager.tsx +367 -0
  90. tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubConnection.tsx +233 -0
  91. tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubErrorBoundary.tsx +105 -0
  92. tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubProgressiveLoader.tsx +266 -0
  93. tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubRepositoryCard.tsx +121 -0
  94. tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubRepositorySelector.tsx +312 -0
  95. tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubStats.tsx +291 -0
  96. tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubUserProfile.tsx +46 -0
  97. tools/bolt.diy/app/components/@settings/tabs/github/components/shared/GitHubStateIndicators.tsx +264 -0
  98. tools/bolt.diy/app/components/@settings/tabs/github/components/shared/RepositoryCard.tsx +361 -0
  99. tools/bolt.diy/app/components/@settings/tabs/github/components/shared/index.ts +11 -0
  100. tools/bolt.diy/app/components/@settings/tabs/gitlab/GitLabTab.tsx +305 -0
  101. tools/bolt.diy/app/components/@settings/tabs/gitlab/components/GitLabAuthDialog.tsx +186 -0
  102. tools/bolt.diy/app/components/@settings/tabs/gitlab/components/GitLabConnection.tsx +253 -0
  103. tools/bolt.diy/app/components/@settings/tabs/gitlab/components/GitLabRepositorySelector.tsx +358 -0
  104. tools/bolt.diy/app/components/@settings/tabs/gitlab/components/RepositoryCard.tsx +79 -0
  105. tools/bolt.diy/app/components/@settings/tabs/gitlab/components/RepositoryList.tsx +142 -0
  106. tools/bolt.diy/app/components/@settings/tabs/gitlab/components/StatsDisplay.tsx +91 -0
  107. tools/bolt.diy/app/components/@settings/tabs/gitlab/components/index.ts +4 -0
  108. tools/bolt.diy/app/components/@settings/tabs/mcp/McpServerList.tsx +99 -0
  109. tools/bolt.diy/app/components/@settings/tabs/mcp/McpServerListItem.tsx +70 -0
  110. tools/bolt.diy/app/components/@settings/tabs/mcp/McpStatusBadge.tsx +37 -0
  111. tools/bolt.diy/app/components/@settings/tabs/mcp/McpTab.tsx +239 -0
  112. tools/bolt.diy/app/components/@settings/tabs/netlify/NetlifyTab.tsx +1393 -0
  113. tools/bolt.diy/app/components/@settings/tabs/netlify/components/NetlifyConnection.tsx +990 -0
  114. tools/bolt.diy/app/components/@settings/tabs/netlify/components/index.ts +1 -0
  115. tools/bolt.diy/app/components/@settings/tabs/notifications/NotificationsTab.tsx +300 -0
  116. tools/bolt.diy/app/components/@settings/tabs/profile/ProfileTab.tsx +181 -0
  117. tools/bolt.diy/app/components/@settings/tabs/providers/cloud/CloudProvidersTab.tsx +308 -0
  118. tools/bolt.diy/app/components/@settings/tabs/providers/local/ErrorBoundary.tsx +68 -0
  119. tools/bolt.diy/app/components/@settings/tabs/providers/local/HealthStatusBadge.tsx +64 -0
  120. tools/bolt.diy/app/components/@settings/tabs/providers/local/LoadingSkeleton.tsx +107 -0
  121. tools/bolt.diy/app/components/@settings/tabs/providers/local/LocalProvidersTab.tsx +556 -0
  122. tools/bolt.diy/app/components/@settings/tabs/providers/local/ModelCard.tsx +106 -0
  123. tools/bolt.diy/app/components/@settings/tabs/providers/local/ProviderCard.tsx +120 -0
  124. tools/bolt.diy/app/components/@settings/tabs/providers/local/SetupGuide.tsx +671 -0
  125. tools/bolt.diy/app/components/@settings/tabs/providers/local/StatusDashboard.tsx +91 -0
  126. tools/bolt.diy/app/components/@settings/tabs/providers/local/types.ts +44 -0
  127. tools/bolt.diy/app/components/@settings/tabs/settings/SettingsTab.tsx +215 -0
  128. tools/bolt.diy/app/components/@settings/tabs/supabase/SupabaseTab.tsx +1089 -0
  129. tools/bolt.diy/app/components/@settings/tabs/vercel/VercelTab.tsx +909 -0
  130. tools/bolt.diy/app/components/@settings/tabs/vercel/components/VercelConnection.tsx +368 -0
  131. tools/bolt.diy/app/components/@settings/tabs/vercel/components/index.ts +1 -0
  132. tools/bolt.diy/app/components/@settings/utils/tab-helpers.ts +54 -0
  133. tools/bolt.diy/app/components/chat/APIKeyManager.tsx +169 -0
  134. tools/bolt.diy/app/components/chat/Artifact.tsx +296 -0
  135. tools/bolt.diy/app/components/chat/AssistantMessage.tsx +192 -0
  136. tools/bolt.diy/app/components/chat/BaseChat.module.scss +47 -0
  137. tools/bolt.diy/app/components/chat/BaseChat.tsx +522 -0
  138. tools/bolt.diy/app/components/chat/Chat.client.tsx +670 -0
  139. tools/bolt.diy/app/components/chat/ChatAlert.tsx +108 -0
  140. tools/bolt.diy/app/components/chat/ChatBox.tsx +334 -0
  141. tools/bolt.diy/app/components/chat/CodeBlock.module.scss +10 -0
  142. tools/bolt.diy/app/components/chat/CodeBlock.tsx +85 -0
  143. tools/bolt.diy/app/components/chat/DicussMode.tsx +17 -0
  144. tools/bolt.diy/app/components/chat/ExamplePrompts.tsx +37 -0
  145. tools/bolt.diy/app/components/chat/FilePreview.tsx +38 -0
  146. tools/bolt.diy/app/components/chat/GitCloneButton.tsx +327 -0
  147. tools/bolt.diy/app/components/chat/ImportFolderButton.tsx +141 -0
  148. tools/bolt.diy/app/components/chat/LLMApiAlert.tsx +109 -0
  149. tools/bolt.diy/app/components/chat/MCPTools.tsx +129 -0
  150. tools/bolt.diy/app/components/chat/Markdown.module.scss +171 -0
  151. tools/bolt.diy/app/components/chat/Markdown.spec.ts +48 -0
  152. tools/bolt.diy/app/components/chat/Markdown.tsx +252 -0
  153. tools/bolt.diy/app/components/chat/Messages.client.tsx +102 -0
  154. tools/bolt.diy/app/components/chat/ModelSelector.tsx +797 -0
  155. tools/bolt.diy/app/components/chat/NetlifyDeploymentLink.client.tsx +51 -0
  156. tools/bolt.diy/app/components/chat/ProgressCompilation.tsx +110 -0
  157. tools/bolt.diy/app/components/chat/ScreenshotStateManager.tsx +33 -0
  158. tools/bolt.diy/app/components/chat/SendButton.client.tsx +39 -0
  159. tools/bolt.diy/app/components/chat/SpeechRecognition.tsx +28 -0
  160. tools/bolt.diy/app/components/chat/StarterTemplates.tsx +38 -0
  161. tools/bolt.diy/app/components/chat/SupabaseAlert.tsx +199 -0
  162. tools/bolt.diy/app/components/chat/SupabaseConnection.tsx +339 -0
  163. tools/bolt.diy/app/components/chat/ThoughtBox.tsx +43 -0
  164. tools/bolt.diy/app/components/chat/ToolInvocations.tsx +409 -0
  165. tools/bolt.diy/app/components/chat/UserMessage.tsx +101 -0
  166. tools/bolt.diy/app/components/chat/VercelDeploymentLink.client.tsx +158 -0
  167. tools/bolt.diy/app/components/chat/chatExportAndImport/ExportChatButton.tsx +49 -0
  168. tools/bolt.diy/app/components/chat/chatExportAndImport/ImportButtons.tsx +96 -0
  169. tools/bolt.diy/app/components/deploy/DeployAlert.tsx +197 -0
  170. tools/bolt.diy/app/components/deploy/DeployButton.tsx +277 -0
  171. tools/bolt.diy/app/components/deploy/GitHubDeploy.client.tsx +171 -0
  172. tools/bolt.diy/app/components/deploy/GitHubDeploymentDialog.tsx +1041 -0
  173. tools/bolt.diy/app/components/deploy/GitLabDeploy.client.tsx +171 -0
  174. tools/bolt.diy/app/components/deploy/GitLabDeploymentDialog.tsx +764 -0
  175. tools/bolt.diy/app/components/deploy/NetlifyDeploy.client.tsx +246 -0
  176. tools/bolt.diy/app/components/deploy/VercelDeploy.client.tsx +235 -0
  177. tools/bolt.diy/app/components/editor/codemirror/BinaryContent.tsx +7 -0
  178. tools/bolt.diy/app/components/editor/codemirror/CodeMirrorEditor.tsx +555 -0
  179. tools/bolt.diy/app/components/editor/codemirror/EnvMasking.ts +80 -0
  180. tools/bolt.diy/app/components/editor/codemirror/cm-theme.ts +192 -0
  181. tools/bolt.diy/app/components/editor/codemirror/indent.ts +68 -0
  182. tools/bolt.diy/app/components/editor/codemirror/languages.ts +112 -0
  183. tools/bolt.diy/app/components/git/GitUrlImport.client.tsx +147 -0
  184. tools/bolt.diy/app/components/header/Header.tsx +42 -0
  185. tools/bolt.diy/app/components/header/HeaderActionButtons.client.tsx +54 -0
  186. tools/bolt.diy/app/components/mandate/MandateSubmission.tsx +167 -0
  187. tools/bolt.diy/app/components/observability/DeploymentStatus.tsx +168 -0
  188. tools/bolt.diy/app/components/observability/EventTimeline.tsx +119 -0
  189. tools/bolt.diy/app/components/observability/FileDiffViewer.tsx +121 -0
  190. tools/bolt.diy/app/components/observability/GovernanceStatus.tsx +197 -0
  191. tools/bolt.diy/app/components/observability/GovernorMetrics.tsx +246 -0
  192. tools/bolt.diy/app/components/observability/LogStream.tsx +244 -0
  193. tools/bolt.diy/app/components/observability/MandateDetails.tsx +201 -0
  194. tools/bolt.diy/app/components/observability/ObservabilityDashboard.tsx +200 -0
  195. tools/bolt.diy/app/components/sidebar/HistoryItem.tsx +187 -0
  196. tools/bolt.diy/app/components/sidebar/Menu.client.tsx +536 -0
  197. tools/bolt.diy/app/components/sidebar/date-binning.ts +59 -0
  198. tools/bolt.diy/app/components/txt +1 -0
  199. tools/bolt.diy/app/components/ui/BackgroundRays/index.tsx +18 -0
  200. tools/bolt.diy/app/components/ui/BackgroundRays/styles.module.scss +246 -0
  201. tools/bolt.diy/app/components/ui/Badge.tsx +53 -0
  202. tools/bolt.diy/app/components/ui/BranchSelector.tsx +270 -0
  203. tools/bolt.diy/app/components/ui/Breadcrumbs.tsx +101 -0
  204. tools/bolt.diy/app/components/ui/Button.tsx +46 -0
  205. tools/bolt.diy/app/components/ui/Card.tsx +55 -0
  206. tools/bolt.diy/app/components/ui/Checkbox.tsx +32 -0
  207. tools/bolt.diy/app/components/ui/CloseButton.tsx +49 -0
  208. tools/bolt.diy/app/components/ui/CodeBlock.tsx +103 -0
  209. tools/bolt.diy/app/components/ui/Collapsible.tsx +9 -0
  210. tools/bolt.diy/app/components/ui/ColorSchemeDialog.tsx +378 -0
  211. tools/bolt.diy/app/components/ui/Dialog.tsx +449 -0
  212. tools/bolt.diy/app/components/ui/Dropdown.tsx +63 -0
  213. tools/bolt.diy/app/components/ui/EmptyState.tsx +154 -0
  214. tools/bolt.diy/app/components/ui/FileIcon.tsx +346 -0
  215. tools/bolt.diy/app/components/ui/FilterChip.tsx +92 -0
  216. tools/bolt.diy/app/components/ui/GlowingEffect.tsx +192 -0
  217. tools/bolt.diy/app/components/ui/GradientCard.tsx +100 -0
  218. tools/bolt.diy/app/components/ui/IconButton.tsx +84 -0
  219. tools/bolt.diy/app/components/ui/Input.tsx +22 -0
  220. tools/bolt.diy/app/components/ui/Label.tsx +20 -0
  221. tools/bolt.diy/app/components/ui/LoadingDots.tsx +27 -0
  222. tools/bolt.diy/app/components/ui/LoadingOverlay.tsx +32 -0
  223. tools/bolt.diy/app/components/ui/PanelHeader.tsx +20 -0
  224. tools/bolt.diy/app/components/ui/PanelHeaderButton.tsx +36 -0
  225. tools/bolt.diy/app/components/ui/Popover.tsx +29 -0
  226. tools/bolt.diy/app/components/ui/Progress.tsx +22 -0
  227. tools/bolt.diy/app/components/ui/RepositoryStats.tsx +87 -0
  228. tools/bolt.diy/app/components/ui/ScrollArea.tsx +41 -0
  229. tools/bolt.diy/app/components/ui/SearchInput.tsx +80 -0
  230. tools/bolt.diy/app/components/ui/SearchResultItem.tsx +134 -0
  231. tools/bolt.diy/app/components/ui/Separator.tsx +22 -0
  232. tools/bolt.diy/app/components/ui/SettingsButton.tsx +35 -0
  233. tools/bolt.diy/app/components/ui/Slider.tsx +73 -0
  234. tools/bolt.diy/app/components/ui/StatusIndicator.tsx +90 -0
  235. tools/bolt.diy/app/components/ui/Switch.tsx +37 -0
  236. tools/bolt.diy/app/components/ui/Tabs.tsx +52 -0
  237. tools/bolt.diy/app/components/ui/TabsWithSlider.tsx +112 -0
  238. tools/bolt.diy/app/components/ui/ThemeSwitch.tsx +29 -0
  239. tools/bolt.diy/app/components/ui/Tooltip.tsx +122 -0
  240. tools/bolt.diy/app/components/ui/index.ts +38 -0
  241. tools/bolt.diy/app/components/ui/use-toast.ts +66 -0
  242. tools/bolt.diy/app/components/workbench/DiffView.tsx +796 -0
  243. tools/bolt.diy/app/components/workbench/EditorPanel.tsx +174 -0
  244. tools/bolt.diy/app/components/workbench/ExpoQrModal.tsx +55 -0
  245. tools/bolt.diy/app/components/workbench/FileBreadcrumb.tsx +150 -0
  246. tools/bolt.diy/app/components/workbench/FileTree.tsx +565 -0
  247. tools/bolt.diy/app/components/workbench/Inspector.tsx +126 -0
  248. tools/bolt.diy/app/components/workbench/InspectorPanel.tsx +146 -0
  249. tools/bolt.diy/app/components/workbench/LockManager.tsx +262 -0
  250. tools/bolt.diy/app/components/workbench/PortDropdown.tsx +91 -0
  251. tools/bolt.diy/app/components/workbench/Preview.tsx +1049 -0
  252. tools/bolt.diy/app/components/workbench/ScreenshotSelector.tsx +293 -0
  253. tools/bolt.diy/app/components/workbench/Search.tsx +257 -0
  254. tools/bolt.diy/app/components/workbench/Workbench.client.tsx +506 -0
  255. tools/bolt.diy/app/components/workbench/terminal/Terminal.tsx +131 -0
  256. tools/bolt.diy/app/components/workbench/terminal/TerminalManager.tsx +68 -0
  257. tools/bolt.diy/app/components/workbench/terminal/TerminalTabs.tsx +277 -0
  258. tools/bolt.diy/app/components/workbench/terminal/theme.ts +36 -0
  259. tools/bolt.diy/app/components/workflow/WorkflowPhase.tsx +109 -0
  260. tools/bolt.diy/app/components/workflow/WorkflowStatus.tsx +60 -0
  261. tools/bolt.diy/app/components/workflow/WorkflowTimeline.tsx +150 -0
  262. tools/bolt.diy/app/entry.client.tsx +7 -0
  263. tools/bolt.diy/app/entry.server.tsx +80 -0
  264. tools/bolt.diy/app/root.tsx +156 -0
  265. tools/bolt.diy/app/routes/_index.tsx +175 -0
  266. tools/bolt.diy/app/routes/api.bug-report.ts +254 -0
  267. tools/bolt.diy/app/routes/api.chat.ts +463 -0
  268. tools/bolt.diy/app/routes/api.check-env-key.ts +41 -0
  269. tools/bolt.diy/app/routes/api.configured-providers.ts +110 -0
  270. tools/bolt.diy/app/routes/api.corporate-swarm-status.ts +55 -0
  271. tools/bolt.diy/app/routes/api.enhancer.ts +137 -0
  272. tools/bolt.diy/app/routes/api.export-api-keys.ts +44 -0
  273. tools/bolt.diy/app/routes/api.git-info.ts +69 -0
  274. tools/bolt.diy/app/routes/api.git-proxy.$.ts +178 -0
  275. tools/bolt.diy/app/routes/api.github-branches.ts +166 -0
  276. tools/bolt.diy/app/routes/api.github-deploy.ts +67 -0
  277. tools/bolt.diy/app/routes/api.github-stats.ts +198 -0
  278. tools/bolt.diy/app/routes/api.github-template.ts +242 -0
  279. tools/bolt.diy/app/routes/api.github-user.ts +287 -0
  280. tools/bolt.diy/app/routes/api.gitlab-branches.ts +143 -0
  281. tools/bolt.diy/app/routes/api.gitlab-deploy.ts +67 -0
  282. tools/bolt.diy/app/routes/api.gitlab-projects.ts +105 -0
  283. tools/bolt.diy/app/routes/api.health.ts +8 -0
  284. tools/bolt.diy/app/routes/api.llmcall.ts +298 -0
  285. tools/bolt.diy/app/routes/api.mandate.ts +351 -0
  286. tools/bolt.diy/app/routes/api.mcp-check.ts +16 -0
  287. tools/bolt.diy/app/routes/api.mcp-update-config.ts +23 -0
  288. tools/bolt.diy/app/routes/api.models.$provider.ts +2 -0
  289. tools/bolt.diy/app/routes/api.models.ts +90 -0
  290. tools/bolt.diy/app/routes/api.netlify-deploy.ts +240 -0
  291. tools/bolt.diy/app/routes/api.netlify-user.ts +142 -0
  292. tools/bolt.diy/app/routes/api.supabase-user.ts +199 -0
  293. tools/bolt.diy/app/routes/api.supabase.query.ts +92 -0
  294. tools/bolt.diy/app/routes/api.supabase.ts +56 -0
  295. tools/bolt.diy/app/routes/api.supabase.variables.ts +32 -0
  296. tools/bolt.diy/app/routes/api.system.diagnostics.ts +142 -0
  297. tools/bolt.diy/app/routes/api.system.disk-info.ts +311 -0
  298. tools/bolt.diy/app/routes/api.system.git-info.ts +332 -0
  299. tools/bolt.diy/app/routes/api.update.ts +21 -0
  300. tools/bolt.diy/app/routes/api.vercel-deploy.ts +497 -0
  301. tools/bolt.diy/app/routes/api.vercel-user.ts +161 -0
  302. tools/bolt.diy/app/routes/api.workflow-status.$proposalId.ts +309 -0
  303. tools/bolt.diy/app/routes/chat.$id.tsx +8 -0
  304. tools/bolt.diy/app/routes/execute.$mandateId.tsx +432 -0
  305. tools/bolt.diy/app/routes/git.tsx +25 -0
  306. tools/bolt.diy/app/routes/observability.$mandateId.tsx +50 -0
  307. tools/bolt.diy/app/routes/webcontainer.connect.$id.tsx +32 -0
  308. tools/bolt.diy/app/routes/webcontainer.preview.$id.tsx +97 -0
  309. tools/bolt.diy/app/routes/workflow.$proposalId.tsx +170 -0
  310. tools/bolt.diy/app/styles/animations.scss +49 -0
  311. tools/bolt.diy/app/styles/components/code.scss +9 -0
  312. tools/bolt.diy/app/styles/components/editor.scss +135 -0
  313. tools/bolt.diy/app/styles/components/resize-handle.scss +30 -0
  314. tools/bolt.diy/app/styles/components/terminal.scss +3 -0
  315. tools/bolt.diy/app/styles/components/toast.scss +23 -0
  316. tools/bolt.diy/app/styles/diff-view.css +72 -0
  317. tools/bolt.diy/app/styles/index.scss +73 -0
  318. tools/bolt.diy/app/styles/variables.scss +255 -0
  319. tools/bolt.diy/app/styles/z-index.scss +37 -0
  320. tools/bolt.diy/app/types/GitHub.ts +182 -0
  321. tools/bolt.diy/app/types/GitLab.ts +103 -0
  322. tools/bolt.diy/app/types/actions.ts +85 -0
  323. tools/bolt.diy/app/types/artifact.ts +5 -0
  324. tools/bolt.diy/app/types/context.ts +26 -0
  325. tools/bolt.diy/app/types/design-scheme.ts +93 -0
  326. tools/bolt.diy/app/types/global.d.ts +13 -0
  327. tools/bolt.diy/app/types/mandate.ts +333 -0
  328. tools/bolt.diy/app/types/model.ts +25 -0
  329. tools/bolt.diy/app/types/netlify.ts +94 -0
  330. tools/bolt.diy/app/types/supabase.ts +54 -0
  331. tools/bolt.diy/app/types/template.ts +8 -0
  332. tools/bolt.diy/app/types/terminal.ts +9 -0
  333. tools/bolt.diy/app/types/theme.ts +1 -0
  334. tools/bolt.diy/app/types/vercel.ts +67 -0
  335. tools/bolt.diy/app/utils/buffer.ts +29 -0
  336. tools/bolt.diy/app/utils/classNames.ts +65 -0
  337. tools/bolt.diy/app/utils/constants.ts +147 -0
  338. tools/bolt.diy/app/utils/debounce.ts +13 -0
  339. tools/bolt.diy/app/utils/debugLogger.ts +1284 -0
  340. tools/bolt.diy/app/utils/diff.spec.ts +11 -0
  341. tools/bolt.diy/app/utils/diff.ts +117 -0
  342. tools/bolt.diy/app/utils/easings.ts +3 -0
  343. tools/bolt.diy/app/utils/fileLocks.ts +96 -0
  344. tools/bolt.diy/app/utils/fileUtils.ts +121 -0
  345. tools/bolt.diy/app/utils/folderImport.ts +73 -0
  346. tools/bolt.diy/app/utils/formatSize.ts +12 -0
  347. tools/bolt.diy/app/utils/getLanguageFromExtension.ts +24 -0
  348. tools/bolt.diy/app/utils/githubStats.ts +9 -0
  349. tools/bolt.diy/app/utils/gitlabStats.ts +54 -0
  350. tools/bolt.diy/app/utils/logger.ts +162 -0
  351. tools/bolt.diy/app/utils/markdown.ts +155 -0
  352. tools/bolt.diy/app/utils/mobile.ts +4 -0
  353. tools/bolt.diy/app/utils/os.ts +4 -0
  354. tools/bolt.diy/app/utils/path.ts +19 -0
  355. tools/bolt.diy/app/utils/projectCommands.ts +197 -0
  356. tools/bolt.diy/app/utils/promises.ts +19 -0
  357. tools/bolt.diy/app/utils/react.ts +6 -0
  358. tools/bolt.diy/app/utils/sampler.ts +49 -0
  359. tools/bolt.diy/app/utils/selectStarterTemplate.ts +255 -0
  360. tools/bolt.diy/app/utils/shell.ts +384 -0
  361. tools/bolt.diy/app/utils/stacktrace.ts +27 -0
  362. tools/bolt.diy/app/utils/stripIndent.ts +23 -0
  363. tools/bolt.diy/app/utils/terminal.ts +11 -0
  364. tools/bolt.diy/app/utils/unreachable.ts +3 -0
  365. tools/bolt.diy/app/vite-env.d.ts +2 -0
  366. tools/bolt.diy/assets/entitlements.mac.plist +25 -0
  367. tools/bolt.diy/assets/icons/icon.icns +0 -0
  368. tools/bolt.diy/assets/icons/icon.ico +0 -0
  369. tools/bolt.diy/assets/icons/icon.png +0 -0
  370. tools/bolt.diy/bindings.js +78 -0
  371. tools/bolt.diy/bindings.sh +33 -0
  372. tools/bolt.diy/docker-compose.yaml +145 -0
  373. tools/bolt.diy/electron/main/index.ts +201 -0
  374. tools/bolt.diy/electron/main/tsconfig.json +30 -0
  375. tools/bolt.diy/electron/main/ui/menu.ts +29 -0
  376. tools/bolt.diy/electron/main/ui/window.ts +54 -0
  377. tools/bolt.diy/electron/main/utils/auto-update.ts +110 -0
  378. tools/bolt.diy/electron/main/utils/constants.ts +4 -0
  379. tools/bolt.diy/electron/main/utils/cookie.ts +40 -0
  380. tools/bolt.diy/electron/main/utils/reload.ts +35 -0
  381. tools/bolt.diy/electron/main/utils/serve.ts +71 -0
  382. tools/bolt.diy/electron/main/utils/store.ts +3 -0
  383. tools/bolt.diy/electron/main/utils/vite-server.ts +44 -0
  384. tools/bolt.diy/electron/main/vite.config.ts +44 -0
  385. tools/bolt.diy/electron/preload/index.ts +22 -0
  386. tools/bolt.diy/electron/preload/tsconfig.json +7 -0
  387. tools/bolt.diy/electron/preload/vite.config.ts +31 -0
  388. tools/bolt.diy/electron-builder.yml +64 -0
  389. tools/bolt.diy/electron-update.yml +4 -0
  390. tools/bolt.diy/eslint.config.mjs +57 -0
  391. tools/bolt.diy/functions/[[path]].ts +12 -0
  392. tools/bolt.diy/icons/angular.svg +1 -0
  393. tools/bolt.diy/icons/astro.svg +8 -0
  394. tools/bolt.diy/icons/chat.svg +1 -0
  395. tools/bolt.diy/icons/expo-brand.svg +1 -0
  396. tools/bolt.diy/icons/expo.svg +4 -0
  397. tools/bolt.diy/icons/logo-text.svg +1 -0
  398. tools/bolt.diy/icons/logo.svg +4 -0
  399. tools/bolt.diy/icons/mcp.svg +1 -0
  400. tools/bolt.diy/icons/nativescript.svg +1 -0
  401. tools/bolt.diy/icons/netlify.svg +10 -0
  402. tools/bolt.diy/icons/nextjs.svg +1 -0
  403. tools/bolt.diy/icons/nuxt.svg +1 -0
  404. tools/bolt.diy/icons/qwik.svg +1 -0
  405. tools/bolt.diy/icons/react.svg +1 -0
  406. tools/bolt.diy/icons/remix.svg +24 -0
  407. tools/bolt.diy/icons/remotion.svg +1 -0
  408. tools/bolt.diy/icons/shadcn.svg +21 -0
  409. tools/bolt.diy/icons/slidev.svg +60 -0
  410. tools/bolt.diy/icons/solidjs.svg +1 -0
  411. tools/bolt.diy/icons/stars.svg +1 -0
  412. tools/bolt.diy/icons/svelte.svg +1 -0
  413. tools/bolt.diy/icons/typescript.svg +1 -0
  414. tools/bolt.diy/icons/vite.svg +1 -0
  415. tools/bolt.diy/icons/vue.svg +1 -0
  416. tools/bolt.diy/load-context.ts +9 -0
  417. tools/bolt.diy/notarize.cjs +31 -0
  418. tools/bolt.diy/package.json +218 -0
  419. tools/bolt.diy/playwright.config.preview.ts +35 -0
  420. tools/bolt.diy/pre-start.cjs +26 -0
  421. tools/bolt.diy/public/apple-touch-icon-precomposed.png +0 -0
  422. tools/bolt.diy/public/apple-touch-icon.png +0 -0
  423. tools/bolt.diy/public/favicon.ico +0 -0
  424. tools/bolt.diy/public/favicon.svg +4 -0
  425. tools/bolt.diy/public/icons/AmazonBedrock.svg +1 -0
  426. tools/bolt.diy/public/icons/Anthropic.svg +4 -0
  427. tools/bolt.diy/public/icons/Cohere.svg +4 -0
  428. tools/bolt.diy/public/icons/Deepseek.svg +5 -0
  429. tools/bolt.diy/public/icons/Default.svg +4 -0
  430. tools/bolt.diy/public/icons/Google.svg +4 -0
  431. tools/bolt.diy/public/icons/Groq.svg +4 -0
  432. tools/bolt.diy/public/icons/HuggingFace.svg +4 -0
  433. tools/bolt.diy/public/icons/Hyperbolic.svg +3 -0
  434. tools/bolt.diy/public/icons/LMStudio.svg +5 -0
  435. tools/bolt.diy/public/icons/Mistral.svg +4 -0
  436. tools/bolt.diy/public/icons/Ollama.svg +4 -0
  437. tools/bolt.diy/public/icons/OpenAI.svg +4 -0
  438. tools/bolt.diy/public/icons/OpenAILike.svg +4 -0
  439. tools/bolt.diy/public/icons/OpenRouter.svg +4 -0
  440. tools/bolt.diy/public/icons/Perplexity.svg +4 -0
  441. tools/bolt.diy/public/icons/Together.svg +4 -0
  442. tools/bolt.diy/public/icons/xAI.svg +5 -0
  443. tools/bolt.diy/public/inspector-script.js +292 -0
  444. tools/bolt.diy/public/logo-dark-styled.png +0 -0
  445. tools/bolt.diy/public/logo-dark.png +0 -0
  446. tools/bolt.diy/public/logo-light-styled.png +0 -0
  447. tools/bolt.diy/public/logo-light.png +0 -0
  448. tools/bolt.diy/public/logo.svg +15 -0
  449. tools/bolt.diy/public/social_preview_index.jpg +0 -0
  450. tools/bolt.diy/scripts/clean.js +45 -0
  451. tools/bolt.diy/scripts/electron-dev.mjs +181 -0
  452. tools/bolt.diy/scripts/setup-env.sh +41 -0
  453. tools/bolt.diy/scripts/update-imports.sh +7 -0
  454. tools/bolt.diy/scripts/update.sh +52 -0
  455. tools/bolt.diy/services/execution-governor/Dockerfile +41 -0
  456. tools/bolt.diy/services/execution-governor/config.ts +42 -0
  457. tools/bolt.diy/services/execution-governor/index.ts +683 -0
  458. tools/bolt.diy/services/execution-governor/metrics.ts +141 -0
  459. tools/bolt.diy/services/execution-governor/package.json +31 -0
  460. tools/bolt.diy/services/execution-governor/priority-queue.ts +139 -0
  461. tools/bolt.diy/services/execution-governor/tsconfig.json +21 -0
  462. tools/bolt.diy/services/execution-governor/types.ts +145 -0
  463. tools/bolt.diy/services/headless-executor/Dockerfile +43 -0
  464. tools/bolt.diy/services/headless-executor/executor.ts +210 -0
  465. tools/bolt.diy/services/headless-executor/index.ts +323 -0
  466. tools/bolt.diy/services/headless-executor/package.json +27 -0
  467. tools/bolt.diy/services/headless-executor/tsconfig.json +21 -0
  468. tools/bolt.diy/services/headless-executor/types.ts +38 -0
  469. tools/bolt.diy/test-workflows.sh +240 -0
  470. tools/bolt.diy/tests/integration/corporate-swarm.test.ts +208 -0
  471. tools/bolt.diy/tests/mandates/budget-limited.json +34 -0
  472. tools/bolt.diy/tests/mandates/complex.json +53 -0
  473. tools/bolt.diy/tests/mandates/constraint-enforced.json +36 -0
  474. tools/bolt.diy/tests/mandates/simple.json +35 -0
  475. tools/bolt.diy/tsconfig.json +37 -0
  476. tools/bolt.diy/types/istextorbinary.d.ts +15 -0
  477. tools/bolt.diy/uno.config.ts +279 -0
  478. tools/bolt.diy/vite-electron.config.ts +76 -0
  479. tools/bolt.diy/vite.config.ts +112 -0
  480. tools/bolt.diy/worker-configuration.d.ts +22 -0
  481. tools/bolt.diy/wrangler.toml +6 -0
  482. tools/code_generator.py +461 -0
  483. tools/file_operations.py +465 -0
  484. tools/mandate_generator.py +337 -0
  485. tools/mcpClientUtils.py +1216 -0
  486. tools/sensors.py +285 -0
  487. utils/Agent_types.py +15 -0
  488. utils/AnyToStr.py +0 -0
  489. utils/HHCS.py +277 -0
  490. utils/__init__.py +30 -0
  491. utils/agent.py +3627 -0
  492. utils/aop.py +2948 -0
  493. utils/canonical.py +143 -0
  494. utils/conversation.py +1195 -0
  495. utils/doctrine_versioning +230 -0
  496. utils/formatter.py +474 -0
  497. utils/ledger.py +311 -0
  498. utils/out_types.py +16 -0
  499. utils/rollback.py +339 -0
  500. utils/router.py +929 -0
  501. utils/tui.py +1908 -0
utils/aop.py ADDED
@@ -0,0 +1,2948 @@
1
+ import asyncio
2
+ from contextlib import AbstractAsyncContextManager
3
+ import socket
4
+ import sys
5
+ import threading
6
+ import time
7
+ import traceback
8
+ from collections import deque
9
+ from dataclasses import dataclass, field
10
+ from enum import Enum
11
+ from typing import Any, Callable, Dict, List, Literal, Optional
12
+ from uuid import uuid4
13
+
14
+ from loguru import logger
15
+ from mcp.server.auth.settings import AuthSettings
16
+ from mcp.server.fastmcp import FastMCP
17
+ from mcp.server.lowlevel.server import LifespanResultT
18
+ from mcp.server.transport_security import TransportSecuritySettings
19
+
20
+ from swarms.structs.agent import Agent
21
+ from swarms.structs.omni_agent_types import AgentType
22
+ from swarms.tools.mcp_client_tools import (
23
+ get_tools_for_multiple_mcp_servers,
24
+ )
25
+
26
+
27
+ class TaskStatus(Enum):
28
+ """Status of a task in the queue."""
29
+
30
+ PENDING = "pending"
31
+ PROCESSING = "processing"
32
+ COMPLETED = "completed"
33
+ FAILED = "failed"
34
+ CANCELLED = "cancelled"
35
+
36
+
37
+ class QueueStatus(Enum):
38
+ """Status of a task queue."""
39
+
40
+ RUNNING = "running"
41
+ PAUSED = "paused"
42
+ STOPPED = "stopped"
43
+
44
+
45
+ @dataclass
46
+ class Task:
47
+ """
48
+ Represents a task to be executed by an agent.
49
+
50
+ Attributes:
51
+ task_id: Unique identifier for the task
52
+ task: The task or prompt to execute
53
+ img: Optional image to be processed
54
+ imgs: Optional list of images to be processed
55
+ correct_answer: Optional correct answer for validation
56
+ priority: Task priority (higher number = higher priority)
57
+ created_at: Timestamp when task was created
58
+ status: Current status of the task
59
+ result: Result of task execution
60
+ error: Error message if task failed
61
+ retry_count: Number of times task has been retried
62
+ max_retries: Maximum number of retries allowed
63
+ """
64
+
65
+ task_id: str = field(default_factory=lambda: str(uuid4()))
66
+ task: str = ""
67
+ img: Optional[str] = None
68
+ imgs: Optional[List[str]] = None
69
+ correct_answer: Optional[str] = None
70
+ priority: int = 0
71
+ created_at: float = field(default_factory=time.time)
72
+ status: TaskStatus = TaskStatus.PENDING
73
+ result: Optional[str] = None
74
+ error: Optional[str] = None
75
+ retry_count: int = 0
76
+ max_retries: int = 3
77
+
78
+
79
+ @dataclass
80
+ class QueueStats:
81
+ """
82
+ Statistics for a task queue.
83
+
84
+ Attributes:
85
+ total_tasks: Total number of tasks processed
86
+ completed_tasks: Number of successfully completed tasks
87
+ failed_tasks: Number of failed tasks
88
+ pending_tasks: Number of tasks currently pending
89
+ processing_tasks: Number of tasks currently being processed
90
+ average_processing_time: Average time to process a task
91
+ queue_size: Current size of the queue
92
+ """
93
+
94
+ total_tasks: int = 0
95
+ completed_tasks: int = 0
96
+ failed_tasks: int = 0
97
+ pending_tasks: int = 0
98
+ processing_tasks: int = 0
99
+ average_processing_time: float = 0.0
100
+ queue_size: int = 0
101
+
102
+
103
+ class TaskQueue:
104
+ """
105
+ A thread-safe task queue for managing agent tasks.
106
+
107
+ This class provides functionality to:
108
+ 1. Add tasks to the queue with priority support
109
+ 2. Process tasks in background workers
110
+ 3. Handle task retries and error management
111
+ 4. Provide queue statistics and monitoring
112
+ """
113
+
114
+ def __init__(
115
+ self,
116
+ agent_name: str,
117
+ agent: AgentType,
118
+ max_workers: int = 1,
119
+ max_queue_size: int = 1000,
120
+ processing_timeout: int = 30,
121
+ retry_delay: float = 1.0,
122
+ verbose: bool = False,
123
+ ):
124
+ """
125
+ Initialize the task queue.
126
+
127
+ Args:
128
+ agent_name: Name of the agent this queue belongs to
129
+ agent: The agent instance to execute tasks
130
+ max_workers: Maximum number of worker threads
131
+ max_queue_size: Maximum number of tasks in queue
132
+ processing_timeout: Timeout for task processing in seconds
133
+ retry_delay: Delay between retries in seconds
134
+ verbose: Enable verbose logging
135
+ """
136
+ self.agent_name = agent_name
137
+ self.agent = agent
138
+ self.max_workers = max_workers
139
+ self.max_queue_size = max_queue_size
140
+ self.processing_timeout = processing_timeout
141
+ self.retry_delay = retry_delay
142
+ self.verbose = verbose
143
+
144
+ # Queue management
145
+ self._queue = deque()
146
+ self._lock = threading.RLock()
147
+ self._status = QueueStatus.STOPPED
148
+ self._workers = []
149
+ self._stop_event = threading.Event()
150
+
151
+ # Statistics
152
+ self._stats = QueueStats()
153
+ self._processing_times = deque(
154
+ maxlen=100
155
+ ) # Keep last 100 processing times
156
+
157
+ # Task tracking
158
+ self._tasks = {} # task_id -> Task
159
+ self._processing_tasks = (
160
+ set()
161
+ ) # Currently processing task IDs
162
+
163
+ logger.info(
164
+ f"Initialized TaskQueue for agent '{agent_name}' with {max_workers} workers"
165
+ )
166
+
167
+ def add_task(
168
+ self,
169
+ task: str,
170
+ img: Optional[str] = None,
171
+ imgs: Optional[List[str]] = None,
172
+ correct_answer: Optional[str] = None,
173
+ priority: int = 0,
174
+ max_retries: int = 3,
175
+ ) -> str:
176
+ """
177
+ Add a task to the queue.
178
+
179
+ Args:
180
+ task: The task or prompt to execute
181
+ img: Optional image to be processed
182
+ imgs: Optional list of images to be processed
183
+ correct_answer: Optional correct answer for validation
184
+ priority: Task priority (higher number = higher priority)
185
+ max_retries: Maximum number of retries allowed
186
+
187
+ Returns:
188
+ str: Task ID
189
+
190
+ Raises:
191
+ ValueError: If queue is full or task is invalid
192
+ """
193
+ if not task:
194
+ raise ValueError("Task cannot be empty")
195
+
196
+ with self._lock:
197
+ if len(self._queue) >= self.max_queue_size:
198
+ raise ValueError(
199
+ f"Queue is full (max size: {self.max_queue_size})"
200
+ )
201
+
202
+ task_obj = Task(
203
+ task=task,
204
+ img=img,
205
+ imgs=imgs,
206
+ correct_answer=correct_answer,
207
+ priority=priority,
208
+ max_retries=max_retries,
209
+ )
210
+
211
+ # Insert task based on priority (higher priority first)
212
+ inserted = False
213
+ for i, existing_task in enumerate(self._queue):
214
+ if task_obj.priority > existing_task.priority:
215
+ self._queue.insert(i, task_obj)
216
+ inserted = True
217
+ break
218
+
219
+ if not inserted:
220
+ self._queue.append(task_obj)
221
+
222
+ self._tasks[task_obj.task_id] = task_obj
223
+ self._stats.total_tasks += 1
224
+ self._stats.pending_tasks += 1
225
+ self._stats.queue_size = len(self._queue)
226
+
227
+ if self.verbose:
228
+ logger.debug(
229
+ f"Added task '{task_obj.task_id}' to queue for agent '{self.agent_name}'"
230
+ )
231
+
232
+ return task_obj.task_id
233
+
234
+ def get_task(self, task_id: str) -> Optional[Task]:
235
+ """
236
+ Get a task by ID.
237
+
238
+ Args:
239
+ task_id: The task ID
240
+
241
+ Returns:
242
+ Task object or None if not found
243
+ """
244
+ with self._lock:
245
+ return self._tasks.get(task_id)
246
+
247
+ def cancel_task(self, task_id: str) -> bool:
248
+ """
249
+ Cancel a task.
250
+
251
+ Args:
252
+ task_id: The task ID to cancel
253
+
254
+ Returns:
255
+ bool: True if task was cancelled, False if not found or already processed
256
+ """
257
+ with self._lock:
258
+ if task_id not in self._tasks:
259
+ return False
260
+
261
+ task = self._tasks[task_id]
262
+ if task.status in [
263
+ TaskStatus.COMPLETED,
264
+ TaskStatus.FAILED,
265
+ TaskStatus.CANCELLED,
266
+ ]:
267
+ return False
268
+
269
+ # Remove from queue if still pending
270
+ if task.status == TaskStatus.PENDING:
271
+ try:
272
+ self._queue.remove(task)
273
+ self._stats.pending_tasks -= 1
274
+ self._stats.queue_size = len(self._queue)
275
+ except ValueError:
276
+ pass # Task not in queue
277
+
278
+ # Mark as cancelled
279
+ task.status = TaskStatus.CANCELLED
280
+ self._processing_tasks.discard(task_id)
281
+
282
+ if self.verbose:
283
+ logger.debug(
284
+ f"Cancelled task '{task_id}' for agent '{self.agent_name}'"
285
+ )
286
+
287
+ return True
288
+
289
+ def start_workers(self) -> None:
290
+ """Start the background worker threads."""
291
+ with self._lock:
292
+ if self._status != QueueStatus.STOPPED:
293
+ logger.warning(
294
+ f"Workers for agent '{self.agent_name}' are already running"
295
+ )
296
+ return
297
+
298
+ self._status = QueueStatus.RUNNING
299
+ self._stop_event.clear()
300
+
301
+ for i in range(self.max_workers):
302
+ worker = threading.Thread(
303
+ target=self._worker_loop,
304
+ name=f"Worker-{self.agent_name}-{i}",
305
+ daemon=True,
306
+ )
307
+ worker.start()
308
+ self._workers.append(worker)
309
+
310
+ logger.info(
311
+ f"Started {self.max_workers} workers for agent '{self.agent_name}'"
312
+ )
313
+
314
+ def stop_workers(self) -> None:
315
+ """Stop the background worker threads."""
316
+ with self._lock:
317
+ if self._status == QueueStatus.STOPPED:
318
+ return
319
+
320
+ self._status = QueueStatus.STOPPED
321
+ self._stop_event.set()
322
+
323
+ # Wait for workers to finish
324
+ for worker in self._workers:
325
+ worker.join(timeout=5.0)
326
+
327
+ self._workers.clear()
328
+ logger.info(
329
+ f"Stopped workers for agent '{self.agent_name}'"
330
+ )
331
+
332
+ def pause_workers(self) -> None:
333
+ """Pause the workers (they will finish current tasks but not start new ones)."""
334
+ with self._lock:
335
+ if self._status == QueueStatus.RUNNING:
336
+ self._status = QueueStatus.PAUSED
337
+ logger.info(
338
+ f"Paused workers for agent '{self.agent_name}'"
339
+ )
340
+
341
+ def resume_workers(self) -> None:
342
+ """Resume the workers."""
343
+ with self._lock:
344
+ if self._status == QueueStatus.PAUSED:
345
+ self._status = QueueStatus.RUNNING
346
+ logger.info(
347
+ f"Resumed workers for agent '{self.agent_name}'"
348
+ )
349
+
350
+ def clear_queue(self) -> int:
351
+ """
352
+ Clear all pending tasks from the queue.
353
+
354
+ Returns:
355
+ int: Number of tasks cleared
356
+ """
357
+ with self._lock:
358
+ cleared_count = len(self._queue)
359
+ self._queue.clear()
360
+ self._stats.pending_tasks = 0
361
+ self._stats.queue_size = 0
362
+
363
+ # Mark all pending tasks as cancelled
364
+ for task in self._tasks.values():
365
+ if task.status == TaskStatus.PENDING:
366
+ task.status = TaskStatus.CANCELLED
367
+
368
+ if self.verbose:
369
+ logger.debug(
370
+ f"Cleared {cleared_count} tasks from queue for agent '{self.agent_name}'"
371
+ )
372
+
373
+ return cleared_count
374
+
375
+ def get_stats(self) -> QueueStats:
376
+ """Get current queue statistics."""
377
+ with self._lock:
378
+ # Update current stats
379
+ self._stats.pending_tasks = len(
380
+ [
381
+ t
382
+ for t in self._tasks.values()
383
+ if t.status == TaskStatus.PENDING
384
+ ]
385
+ )
386
+ self._stats.processing_tasks = len(self._processing_tasks)
387
+ self._stats.queue_size = len(self._queue)
388
+
389
+ # Calculate average processing time
390
+ if self._processing_times:
391
+ self._stats.average_processing_time = sum(
392
+ self._processing_times
393
+ ) / len(self._processing_times)
394
+
395
+ return QueueStats(
396
+ total_tasks=self._stats.total_tasks,
397
+ completed_tasks=self._stats.completed_tasks,
398
+ failed_tasks=self._stats.failed_tasks,
399
+ pending_tasks=self._stats.pending_tasks,
400
+ processing_tasks=self._stats.processing_tasks,
401
+ average_processing_time=self._stats.average_processing_time,
402
+ queue_size=self._stats.queue_size,
403
+ )
404
+
405
+ def get_status(self) -> QueueStatus:
406
+ """Get current queue status."""
407
+ return self._status
408
+
409
+ def _worker_loop(self) -> None:
410
+ """Main worker loop for processing tasks."""
411
+ while not self._stop_event.is_set():
412
+ try:
413
+ # Check if we should process tasks
414
+ with self._lock:
415
+ if (
416
+ self._status != QueueStatus.RUNNING
417
+ or not self._queue
418
+ ):
419
+ self._stop_event.wait(0.1)
420
+ continue
421
+
422
+ # Get next task
423
+ task = self._queue.popleft()
424
+ self._processing_tasks.add(task.task_id)
425
+ task.status = TaskStatus.PROCESSING
426
+ self._stats.pending_tasks -= 1
427
+ self._stats.processing_tasks += 1
428
+
429
+ # Process the task
430
+ self._process_task(task)
431
+
432
+ except Exception as e:
433
+ logger.error(
434
+ f"Error in worker loop for agent '{self.agent_name}': {e}"
435
+ )
436
+ if self.verbose:
437
+ logger.error(traceback.format_exc())
438
+ time.sleep(0.1)
439
+
440
+ def _process_task(self, task: Task) -> None:
441
+ """
442
+ Process a single task.
443
+
444
+ Args:
445
+ task: The task to process
446
+ """
447
+ start_time = time.time()
448
+
449
+ try:
450
+ if self.verbose:
451
+ logger.debug(
452
+ f"Processing task '{task.task_id}' for agent '{self.agent_name}'"
453
+ )
454
+
455
+ # Execute the agent
456
+ result = self.agent.run(
457
+ task=task.task,
458
+ img=task.img,
459
+ imgs=task.imgs,
460
+ correct_answer=task.correct_answer,
461
+ )
462
+
463
+ # Update task with result
464
+ task.result = result
465
+ task.status = TaskStatus.COMPLETED
466
+
467
+ # Update statistics
468
+ processing_time = time.time() - start_time
469
+ self._processing_times.append(processing_time)
470
+
471
+ with self._lock:
472
+ self._stats.completed_tasks += 1
473
+ self._stats.processing_tasks -= 1
474
+ self._processing_tasks.discard(task.task_id)
475
+
476
+ if self.verbose:
477
+ logger.debug(
478
+ f"Completed task '{task.task_id}' in {processing_time:.2f}s"
479
+ )
480
+
481
+ except Exception as e:
482
+ error_msg = str(e)
483
+ task.error = error_msg
484
+ task.retry_count += 1
485
+
486
+ if self.verbose:
487
+ logger.error(
488
+ f"Error processing task '{task.task_id}': {error_msg}"
489
+ )
490
+ logger.error(traceback.format_exc())
491
+
492
+ # Handle retries
493
+ if task.retry_count <= task.max_retries:
494
+ if self.verbose:
495
+ logger.debug(
496
+ f"Retrying task '{task.task_id}' (attempt {task.retry_count + 1})"
497
+ )
498
+
499
+ # Re-queue the task with a delay
500
+ time.sleep(self.retry_delay)
501
+
502
+ with self._lock:
503
+ if self._status == QueueStatus.RUNNING:
504
+ task.status = TaskStatus.PENDING
505
+ self._queue.append(
506
+ task
507
+ ) # Add to end of queue
508
+ self._stats.pending_tasks += 1
509
+ self._stats.queue_size = len(self._queue)
510
+ else:
511
+ task.status = TaskStatus.FAILED
512
+ self._stats.failed_tasks += 1
513
+ else:
514
+ # Max retries exceeded
515
+ task.status = TaskStatus.FAILED
516
+
517
+ with self._lock:
518
+ self._stats.failed_tasks += 1
519
+ self._stats.processing_tasks -= 1
520
+ self._processing_tasks.discard(task.task_id)
521
+
522
+ if self.verbose:
523
+ logger.error(
524
+ f"Task '{task.task_id}' failed after {task.max_retries} retries"
525
+ )
526
+
527
+
528
+ @dataclass
529
+ class AgentToolConfig:
530
+ """
531
+ Configuration for converting an agent to an MCP tool.
532
+
533
+ Attributes:
534
+ tool_name: The name of the tool in the MCP server
535
+ tool_description: Description of what the tool does
536
+ input_schema: JSON schema for the tool's input parameters
537
+ output_schema: JSON schema for the tool's output
538
+ timeout: Maximum time to wait for agent execution (seconds)
539
+ max_retries: Number of retries if agent execution fails
540
+ verbose: Enable verbose logging for this tool
541
+ traceback_enabled: Enable traceback logging for errors
542
+ """
543
+
544
+ tool_name: str
545
+ tool_description: str
546
+ input_schema: Dict[str, Any]
547
+ output_schema: Dict[str, Any]
548
+ timeout: int = 30
549
+ max_retries: int = 3
550
+ verbose: bool = False
551
+ traceback_enabled: bool = True
552
+
553
+
554
+ class AOP:
555
+ """
556
+ A class that takes a list of agents and deploys them as unique tools in an MCP server.
557
+
558
+ This class provides functionality to:
559
+ 1. Convert swarms agents into MCP tools
560
+ 2. Deploy multiple agents as individual tools
561
+ 3. Handle tool execution with proper error handling
562
+ 4. Manage the MCP server lifecycle
563
+ 5. Queue-based task execution for improved performance and reliability
564
+ 6. Persistence mode with automatic restart and failsafe protection
565
+
566
+ Attributes:
567
+ mcp_server: The FastMCP server instance
568
+ agents: Dictionary mapping tool names to agent instances
569
+ tool_configs: Dictionary mapping tool names to their configurations
570
+ task_queues: Dictionary mapping tool names to their task queues
571
+ server_name: Name of the MCP server
572
+ queue_enabled: Whether queue-based execution is enabled
573
+ persistence: Whether persistence mode is enabled
574
+ max_restart_attempts: Maximum number of restart attempts before giving up
575
+ restart_delay: Delay between restart attempts in seconds
576
+ network_monitoring: Whether network connection monitoring is enabled
577
+ max_network_retries: Maximum number of network reconnection attempts
578
+ network_retry_delay: Delay between network retry attempts in seconds
579
+ network_timeout: Network connection timeout in seconds
580
+ """
581
+
582
+ def __init__(
583
+ self,
584
+ server_name: str = "AOP Cluster",
585
+ description: str = "A cluster that enables you to deploy multiple agents as tools in an MCP server.",
586
+ agents: any = None,
587
+ port: int = 8000,
588
+ transport: str = "streamable-http",
589
+ verbose: bool = False,
590
+ traceback_enabled: bool = True,
591
+ host: str = "localhost",
592
+ queue_enabled: bool = True,
593
+ max_workers_per_agent: int = 1,
594
+ max_queue_size_per_agent: int = 1000,
595
+ processing_timeout: int = 30,
596
+ retry_delay: float = 1.0,
597
+ persistence: bool = False,
598
+ max_restart_attempts: int = 10,
599
+ restart_delay: float = 5.0,
600
+ network_monitoring: bool = True,
601
+ max_network_retries: int = 5,
602
+ network_retry_delay: float = 10.0,
603
+ network_timeout: float = 30.0,
604
+ log_level: Literal[
605
+ "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"
606
+ ] = "INFO",
607
+ lifespan: (
608
+ Callable[
609
+ [FastMCP[LifespanResultT]],
610
+ AbstractAsyncContextManager[LifespanResultT],
611
+ ]
612
+ | None
613
+ ) = None,
614
+ auth: AuthSettings | None = None,
615
+ transport_security: TransportSecuritySettings | None = None,
616
+ *args,
617
+ **kwargs,
618
+ ):
619
+ """
620
+ Initialize the AOP.
621
+
622
+ Args:
623
+ server_name: Name for the MCP server
624
+ description: Description of the AOP cluster
625
+ agents: Optional list of agents to add initially
626
+ port: Port for the MCP server
627
+ transport: Transport type for the MCP server
628
+ verbose: Enable verbose logging
629
+ traceback_enabled: Enable traceback logging for errors
630
+ host: Host to bind the server to
631
+ log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
632
+ queue_enabled: Enable queue-based task execution
633
+ max_workers_per_agent: Maximum number of workers per agent
634
+ max_queue_size_per_agent: Maximum queue size per agent
635
+ processing_timeout: Timeout for task processing in seconds
636
+ retry_delay: Delay between retries in seconds
637
+ persistence: Enable automatic restart on shutdown (with failsafe)
638
+ max_restart_attempts: Maximum number of restart attempts before giving up
639
+ restart_delay: Delay between restart attempts in seconds
640
+ network_monitoring: Enable network connection monitoring and retry
641
+ max_network_retries: Maximum number of network reconnection attempts
642
+ network_retry_delay: Delay between network retry attempts in seconds
643
+ network_timeout: Network connection timeout in seconds
644
+ """
645
+ self.server_name = server_name
646
+ self.description = description
647
+ self.verbose = verbose
648
+ self.traceback_enabled = traceback_enabled
649
+ self.log_level = log_level
650
+ self.host = host
651
+ self.port = port
652
+ self.queue_enabled = queue_enabled
653
+ self.max_workers_per_agent = max_workers_per_agent
654
+ self.max_queue_size_per_agent = max_queue_size_per_agent
655
+ self.processing_timeout = processing_timeout
656
+ self.retry_delay = retry_delay
657
+ self.persistence = persistence
658
+ self.max_restart_attempts = max_restart_attempts
659
+ self.restart_delay = restart_delay
660
+ self.network_monitoring = network_monitoring
661
+ self.max_network_retries = max_network_retries
662
+ self.network_retry_delay = network_retry_delay
663
+ self.network_timeout = network_timeout
664
+
665
+ # Persistence state tracking
666
+ self._restart_count = 0
667
+ self._persistence_enabled = persistence
668
+ self._shutdown_requested = False
669
+
670
+ # Network state tracking
671
+ self._network_retry_count = 0
672
+ self._last_network_error = None
673
+ self._network_connected = True
674
+
675
+ # Server creation timestamp
676
+ self._created_at = time.time()
677
+
678
+ self.agents: Dict[str, Agent] = {}
679
+ self.tool_configs: Dict[str, AgentToolConfig] = {}
680
+ self.task_queues: Dict[str, TaskQueue] = {}
681
+ self.transport = transport
682
+
683
+ self.mcp_server = FastMCP(
684
+ name=server_name,
685
+ port=port,
686
+ log_level=log_level,
687
+ lifespan=lifespan,
688
+ auth=auth,
689
+ transport_security=transport_security,
690
+ *args,
691
+ **kwargs,
692
+ )
693
+
694
+ # Configure logger
695
+ logger.remove() # Remove default handler
696
+ logger.add(
697
+ sys.stderr,
698
+ level=log_level,
699
+ format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
700
+ colorize=True,
701
+ )
702
+
703
+ logger.info(
704
+ f"Initialized AOP with server name: {server_name}, verbose: {verbose}, traceback: {traceback_enabled}, persistence: {persistence}, network_monitoring: {network_monitoring}"
705
+ )
706
+
707
+ # Add initial agents if provided
708
+ if agents:
709
+ logger.info(f"Adding {len(agents)} initial agents")
710
+ self.add_agents_batch(agents)
711
+
712
+ # Register the agent discovery tool
713
+ self._register_agent_discovery_tool()
714
+
715
+ # Register queue management tools if queue is enabled
716
+ if self.queue_enabled:
717
+ self._register_queue_management_tools()
718
+
719
+ def add_agent(
720
+ self,
721
+ agent: AgentType,
722
+ tool_name: Optional[str] = None,
723
+ tool_description: Optional[str] = None,
724
+ input_schema: Optional[Dict[str, Any]] = None,
725
+ output_schema: Optional[Dict[str, Any]] = None,
726
+ timeout: int = 30,
727
+ max_retries: int = 3,
728
+ verbose: Optional[bool] = None,
729
+ traceback_enabled: Optional[bool] = None,
730
+ ) -> str:
731
+ """
732
+ Add an agent to the MCP server as a tool.
733
+
734
+ Args:
735
+ agent: The swarms Agent instance to deploy
736
+ tool_name: Name for the tool (defaults to agent.agent_name)
737
+ tool_description: Description of the tool (defaults to agent.agent_description)
738
+ input_schema: JSON schema for input parameters
739
+ output_schema: JSON schema for output
740
+ timeout: Maximum execution time in seconds
741
+ max_retries: Number of retries on failure
742
+ verbose: Enable verbose logging for this tool (defaults to deployer's verbose setting)
743
+ traceback_enabled: Enable traceback logging for this tool (defaults to deployer's setting)
744
+
745
+ Returns:
746
+ str: The tool name that was registered
747
+
748
+ Raises:
749
+ ValueError: If agent is None or tool_name already exists
750
+ """
751
+ if agent is None:
752
+ logger.error("Cannot add None agent")
753
+ raise ValueError("Agent cannot be None")
754
+
755
+ # Use agent name as tool name if not provided
756
+ if tool_name is None:
757
+ tool_name = (
758
+ agent.agent_name or f"agent_{len(self.agents)}"
759
+ )
760
+
761
+ if tool_name in self.agents:
762
+ logger.error(f"Tool name '{tool_name}' already exists")
763
+ raise ValueError(
764
+ f"Tool name '{tool_name}' already exists"
765
+ )
766
+
767
+ # Use deployer defaults if not specified
768
+ if verbose is None:
769
+ verbose = self.verbose
770
+ if traceback_enabled is None:
771
+ traceback_enabled = self.traceback_enabled
772
+
773
+ logger.debug(
774
+ f"Adding agent '{agent.agent_name}' as tool '{tool_name}' with verbose={verbose}, traceback={traceback_enabled}"
775
+ )
776
+
777
+ # Use agent description if not provided
778
+ if tool_description is None:
779
+ tool_description = (
780
+ agent.agent_description
781
+ or f"Agent tool: {agent.agent_name}"
782
+ )
783
+
784
+ # Default input schema for task-based agents
785
+ if input_schema is None:
786
+ input_schema = {
787
+ "type": "object",
788
+ "properties": {
789
+ "task": {
790
+ "type": "string",
791
+ "description": "The task or prompt to execute with this agent",
792
+ },
793
+ "img": {
794
+ "type": "string",
795
+ "description": "Optional image to be processed by the agent",
796
+ },
797
+ "imgs": {
798
+ "type": "array",
799
+ "items": {"type": "string"},
800
+ "description": "Optional list of images to be processed by the agent",
801
+ },
802
+ "correct_answer": {
803
+ "type": "string",
804
+ "description": "Optional correct answer for validation or comparison",
805
+ },
806
+ },
807
+ "required": ["task"],
808
+ }
809
+
810
+ # Default output schema
811
+ if output_schema is None:
812
+ output_schema = {
813
+ "type": "object",
814
+ "properties": {
815
+ "result": {
816
+ "type": "string",
817
+ "description": "The agent's response to the task",
818
+ },
819
+ "success": {
820
+ "type": "boolean",
821
+ "description": "Whether the task was executed successfully",
822
+ },
823
+ "error": {
824
+ "type": "string",
825
+ "description": "Error message if execution failed",
826
+ },
827
+ },
828
+ "required": ["result", "success"],
829
+ }
830
+
831
+ # Store agent and configuration
832
+ self.agents[tool_name] = agent
833
+ self.tool_configs[tool_name] = AgentToolConfig(
834
+ tool_name=tool_name,
835
+ tool_description=tool_description,
836
+ input_schema=input_schema,
837
+ output_schema=output_schema,
838
+ timeout=timeout,
839
+ max_retries=max_retries,
840
+ verbose=verbose,
841
+ traceback_enabled=traceback_enabled,
842
+ )
843
+
844
+ # Create task queue if queue is enabled
845
+ if self.queue_enabled:
846
+ self.task_queues[tool_name] = TaskQueue(
847
+ agent_name=tool_name,
848
+ agent=agent,
849
+ max_workers=self.max_workers_per_agent,
850
+ max_queue_size=self.max_queue_size_per_agent,
851
+ processing_timeout=self.processing_timeout,
852
+ retry_delay=self.retry_delay,
853
+ verbose=verbose,
854
+ )
855
+ # Start the queue workers
856
+ self.task_queues[tool_name].start_workers()
857
+
858
+ # Register the tool with the MCP server
859
+ self._register_tool(tool_name, agent)
860
+
861
+ # Re-register the discovery tool to include the new agent
862
+ self._register_agent_discovery_tool()
863
+
864
+ logger.info(
865
+ f"Added agent '{agent.agent_name}' as tool '{tool_name}' (verbose={verbose}, traceback={traceback_enabled}, queue_enabled={self.queue_enabled})"
866
+ )
867
+ return tool_name
868
+
869
+ def add_agents_batch(
870
+ self,
871
+ agents: List[Agent],
872
+ tool_names: Optional[List[str]] = None,
873
+ tool_descriptions: Optional[List[str]] = None,
874
+ input_schemas: Optional[List[Dict[str, Any]]] = None,
875
+ output_schemas: Optional[List[Dict[str, Any]]] = None,
876
+ timeouts: Optional[List[int]] = None,
877
+ max_retries_list: Optional[List[int]] = None,
878
+ verbose_list: Optional[List[bool]] = None,
879
+ traceback_enabled_list: Optional[List[bool]] = None,
880
+ ) -> List[str]:
881
+ """
882
+ Add multiple agents to the MCP server as tools in batch.
883
+
884
+ Args:
885
+ agents: List of swarms Agent instances
886
+ tool_names: Optional list of tool names (defaults to agent names)
887
+ tool_descriptions: Optional list of tool descriptions
888
+ input_schemas: Optional list of input schemas
889
+ output_schemas: Optional list of output schemas
890
+ timeouts: Optional list of timeout values
891
+ max_retries_list: Optional list of max retry values
892
+ verbose_list: Optional list of verbose settings for each agent
893
+ traceback_enabled_list: Optional list of traceback settings for each agent
894
+
895
+ Returns:
896
+ List[str]: List of tool names that were registered
897
+
898
+ Raises:
899
+ ValueError: If agents list is empty or contains None values
900
+ """
901
+ if not agents:
902
+ logger.error("Cannot add empty agents list")
903
+ raise ValueError("Agents list cannot be empty")
904
+
905
+ if None in agents:
906
+ logger.error("Agents list contains None values")
907
+ raise ValueError("Agents list cannot contain None values")
908
+
909
+ logger.info(f"Adding {len(agents)} agents in batch")
910
+ registered_tools = []
911
+
912
+ for i, agent in enumerate(agents):
913
+ tool_name = (
914
+ tool_names[i]
915
+ if tool_names and i < len(tool_names)
916
+ else None
917
+ )
918
+ tool_description = (
919
+ tool_descriptions[i]
920
+ if tool_descriptions and i < len(tool_descriptions)
921
+ else None
922
+ )
923
+ input_schema = (
924
+ input_schemas[i]
925
+ if input_schemas and i < len(input_schemas)
926
+ else None
927
+ )
928
+ output_schema = (
929
+ output_schemas[i]
930
+ if output_schemas and i < len(output_schemas)
931
+ else None
932
+ )
933
+ timeout = (
934
+ timeouts[i] if timeouts and i < len(timeouts) else 30
935
+ )
936
+ max_retries = (
937
+ max_retries_list[i]
938
+ if max_retries_list and i < len(max_retries_list)
939
+ else 3
940
+ )
941
+ verbose = (
942
+ verbose_list[i]
943
+ if verbose_list and i < len(verbose_list)
944
+ else None
945
+ )
946
+ traceback_enabled = (
947
+ traceback_enabled_list[i]
948
+ if traceback_enabled_list
949
+ and i < len(traceback_enabled_list)
950
+ else None
951
+ )
952
+
953
+ tool_name = self.add_agent(
954
+ agent=agent,
955
+ tool_name=tool_name,
956
+ tool_description=tool_description,
957
+ input_schema=input_schema,
958
+ output_schema=output_schema,
959
+ timeout=timeout,
960
+ max_retries=max_retries,
961
+ verbose=verbose,
962
+ traceback_enabled=traceback_enabled,
963
+ )
964
+ registered_tools.append(tool_name)
965
+
966
+ # Re-register the discovery tool to include all new agents
967
+ self._register_agent_discovery_tool()
968
+
969
+ logger.info(
970
+ f"Added {len(agents)} agents as tools: {registered_tools}"
971
+ )
972
+ return registered_tools
973
+
974
+ def _register_tool(
975
+ self, tool_name: str, agent: AgentType
976
+ ) -> None:
977
+ """
978
+ Register a single agent as an MCP tool.
979
+
980
+ Args:
981
+ tool_name: Name of the tool to register
982
+ agent: The agent instance to register
983
+ """
984
+ config = self.tool_configs[tool_name]
985
+
986
+ @self.mcp_server.tool(
987
+ name=tool_name, description=config.tool_description
988
+ )
989
+ def agent_tool(
990
+ task: str = None,
991
+ img: str = None,
992
+ imgs: List[str] = None,
993
+ correct_answer: str = None,
994
+ max_retries: int = None,
995
+ ) -> Dict[str, Any]:
996
+ """
997
+ Execute the agent with the provided parameters.
998
+
999
+ Args:
1000
+ task: The task or prompt to execute with this agent
1001
+ img: Optional image to be processed by the agent
1002
+ imgs: Optional list of images to be processed by the agent
1003
+ correct_answer: Optional correct answer for validation or comparison
1004
+ max_retries: Maximum number of retries (uses config default if None)
1005
+
1006
+ Returns:
1007
+ Dict containing the agent's response and execution status
1008
+ """
1009
+ start_time = None
1010
+ if config.verbose:
1011
+ start_time = (
1012
+ asyncio.get_event_loop().time()
1013
+ if asyncio.get_event_loop().is_running()
1014
+ else 0
1015
+ )
1016
+ logger.debug(
1017
+ f"Starting execution of tool '{tool_name}' with task: {task[:100] if task else 'None'}..."
1018
+ )
1019
+ if img:
1020
+ logger.debug(f"Processing single image: {img}")
1021
+ if imgs:
1022
+ logger.debug(
1023
+ f"Processing {len(imgs)} images: {imgs}"
1024
+ )
1025
+ if correct_answer:
1026
+ logger.debug(
1027
+ f"Using correct answer for validation: {correct_answer[:50]}..."
1028
+ )
1029
+
1030
+ try:
1031
+ # Validate required parameters
1032
+ if not task:
1033
+ error_msg = "No task provided"
1034
+ logger.warning(
1035
+ f"Tool '{tool_name}' called without task parameter"
1036
+ )
1037
+ return {
1038
+ "result": "",
1039
+ "success": False,
1040
+ "error": error_msg,
1041
+ }
1042
+
1043
+ # Use queue-based execution if enabled
1044
+ if (
1045
+ self.queue_enabled
1046
+ and tool_name in self.task_queues
1047
+ ):
1048
+ return self._execute_with_queue(
1049
+ tool_name,
1050
+ task,
1051
+ img,
1052
+ imgs,
1053
+ correct_answer,
1054
+ 0,
1055
+ max_retries,
1056
+ True,
1057
+ config,
1058
+ )
1059
+ else:
1060
+ # Fallback to direct execution
1061
+ result = self._execute_agent_with_timeout(
1062
+ agent,
1063
+ task,
1064
+ config.timeout,
1065
+ img,
1066
+ imgs,
1067
+ correct_answer,
1068
+ )
1069
+
1070
+ if config.verbose and start_time:
1071
+ execution_time = (
1072
+ asyncio.get_event_loop().time()
1073
+ - start_time
1074
+ if asyncio.get_event_loop().is_running()
1075
+ else 0
1076
+ )
1077
+ logger.debug(
1078
+ f"Tool '{tool_name}' completed successfully in {execution_time:.2f}s"
1079
+ )
1080
+
1081
+ return {
1082
+ "result": str(result),
1083
+ "success": True,
1084
+ "error": None,
1085
+ }
1086
+
1087
+ except Exception as e:
1088
+ error_msg = str(e)
1089
+ logger.error(
1090
+ f"Error executing agent '{tool_name}': {error_msg}"
1091
+ )
1092
+
1093
+ if config.traceback_enabled:
1094
+ logger.error(f"Traceback for tool '{tool_name}':")
1095
+ logger.error(traceback.format_exc())
1096
+
1097
+ if config.verbose and start_time:
1098
+ execution_time = (
1099
+ asyncio.get_event_loop().time() - start_time
1100
+ if asyncio.get_event_loop().is_running()
1101
+ else 0
1102
+ )
1103
+ logger.debug(
1104
+ f"Tool '{tool_name}' failed after {execution_time:.2f}s"
1105
+ )
1106
+
1107
+ return {
1108
+ "result": "",
1109
+ "success": False,
1110
+ "error": error_msg,
1111
+ }
1112
+
1113
+ def _execute_with_queue(
1114
+ self,
1115
+ tool_name: str,
1116
+ task: str,
1117
+ img: Optional[str],
1118
+ imgs: Optional[List[str]],
1119
+ correct_answer: Optional[str],
1120
+ priority: int,
1121
+ max_retries: Optional[int],
1122
+ wait_for_completion: bool,
1123
+ config: AgentToolConfig,
1124
+ ) -> Dict[str, Any]:
1125
+ """
1126
+ Execute a task using the queue system.
1127
+
1128
+ Args:
1129
+ tool_name: Name of the tool/agent
1130
+ task: The task to execute
1131
+ img: Optional image to process
1132
+ imgs: Optional list of images to process
1133
+ correct_answer: Optional correct answer for validation
1134
+ priority: Task priority
1135
+ max_retries: Maximum number of retries
1136
+ wait_for_completion: Whether to wait for completion
1137
+ config: Tool configuration
1138
+
1139
+ Returns:
1140
+ Dict containing the result or task information
1141
+ """
1142
+ # Safety check: ensure queue is enabled
1143
+ if not self.queue_enabled:
1144
+ logger.error(
1145
+ f"Queue execution attempted but queue is disabled for tool '{tool_name}'"
1146
+ )
1147
+ return {
1148
+ "result": "",
1149
+ "success": False,
1150
+ "error": "Queue system is disabled",
1151
+ }
1152
+
1153
+ # Safety check: ensure task queue exists
1154
+ if tool_name not in self.task_queues:
1155
+ logger.error(
1156
+ f"Task queue not found for tool '{tool_name}'"
1157
+ )
1158
+ return {
1159
+ "result": "",
1160
+ "success": False,
1161
+ "error": f"Task queue not found for agent '{tool_name}'",
1162
+ }
1163
+
1164
+ try:
1165
+ # Use config max_retries if not specified
1166
+ if max_retries is None:
1167
+ max_retries = config.max_retries
1168
+
1169
+ # Add task to queue
1170
+ task_id = self.task_queues[tool_name].add_task(
1171
+ task=task,
1172
+ img=img,
1173
+ imgs=imgs,
1174
+ correct_answer=correct_answer,
1175
+ priority=priority,
1176
+ max_retries=max_retries,
1177
+ )
1178
+
1179
+ if not wait_for_completion:
1180
+ # Return task ID immediately
1181
+ return {
1182
+ "task_id": task_id,
1183
+ "status": "queued",
1184
+ "success": True,
1185
+ "message": f"Task '{task_id}' queued for agent '{tool_name}'",
1186
+ }
1187
+
1188
+ # Wait for task completion
1189
+ return self._wait_for_task_completion(
1190
+ tool_name, task_id, config.timeout
1191
+ )
1192
+
1193
+ except Exception as e:
1194
+ error_msg = str(e)
1195
+ logger.error(
1196
+ f"Error adding task to queue for '{tool_name}': {error_msg}"
1197
+ )
1198
+ return {
1199
+ "result": "",
1200
+ "success": False,
1201
+ "error": error_msg,
1202
+ }
1203
+
1204
+ def _wait_for_task_completion(
1205
+ self, tool_name: str, task_id: str, timeout: int
1206
+ ) -> Dict[str, Any]:
1207
+ """
1208
+ Wait for a task to complete.
1209
+
1210
+ Args:
1211
+ tool_name: Name of the tool/agent
1212
+ task_id: ID of the task to wait for
1213
+ timeout: Maximum time to wait in seconds
1214
+
1215
+ Returns:
1216
+ Dict containing the task result
1217
+ """
1218
+ # Safety check: ensure queue is enabled
1219
+ if not self.queue_enabled:
1220
+ logger.error(
1221
+ f"Task completion wait attempted but queue is disabled for tool '{tool_name}'"
1222
+ )
1223
+ return {
1224
+ "result": "",
1225
+ "success": False,
1226
+ "error": "Queue system is disabled",
1227
+ "task_id": task_id,
1228
+ }
1229
+
1230
+ # Safety check: ensure task queue exists
1231
+ if tool_name not in self.task_queues:
1232
+ logger.error(
1233
+ f"Task queue not found for tool '{tool_name}'"
1234
+ )
1235
+ return {
1236
+ "result": "",
1237
+ "success": False,
1238
+ "error": f"Task queue not found for agent '{tool_name}'",
1239
+ "task_id": task_id,
1240
+ }
1241
+
1242
+ start_time = time.time()
1243
+
1244
+ while time.time() - start_time < timeout:
1245
+ task = self.task_queues[tool_name].get_task(task_id)
1246
+ if not task:
1247
+ return {
1248
+ "result": "",
1249
+ "success": False,
1250
+ "error": f"Task '{task_id}' not found",
1251
+ }
1252
+
1253
+ if task.status == TaskStatus.COMPLETED:
1254
+ return {
1255
+ "result": task.result or "",
1256
+ "success": True,
1257
+ "error": None,
1258
+ "task_id": task_id,
1259
+ }
1260
+ elif task.status == TaskStatus.FAILED:
1261
+ return {
1262
+ "result": "",
1263
+ "success": False,
1264
+ "error": task.error or "Task failed",
1265
+ "task_id": task_id,
1266
+ }
1267
+ elif task.status == TaskStatus.CANCELLED:
1268
+ return {
1269
+ "result": "",
1270
+ "success": False,
1271
+ "error": "Task was cancelled",
1272
+ "task_id": task_id,
1273
+ }
1274
+
1275
+ # Wait a bit before checking again
1276
+ time.sleep(0.1)
1277
+
1278
+ # Timeout reached
1279
+ return {
1280
+ "result": "",
1281
+ "success": False,
1282
+ "error": f"Task '{task_id}' timed out after {timeout} seconds",
1283
+ "task_id": task_id,
1284
+ }
1285
+
1286
+ def _execute_agent_with_timeout(
1287
+ self,
1288
+ agent: AgentType,
1289
+ task: str,
1290
+ timeout: int,
1291
+ img: str = None,
1292
+ imgs: List[str] = None,
1293
+ correct_answer: str = None,
1294
+ ) -> str:
1295
+ """
1296
+ Execute an agent with a timeout and all run method parameters.
1297
+
1298
+ Args:
1299
+ agent: The agent to execute
1300
+ task: The task to execute
1301
+ timeout: Maximum execution time in seconds
1302
+ img: Optional image to be processed by the agent
1303
+ imgs: Optional list of images to be processed by the agent
1304
+ correct_answer: Optional correct answer for validation or comparison
1305
+
1306
+ Returns:
1307
+ str: The agent's response
1308
+
1309
+ Raises:
1310
+ TimeoutError: If execution exceeds timeout
1311
+ Exception: If agent execution fails
1312
+ """
1313
+ try:
1314
+ logger.debug(
1315
+ f"Executing agent '{agent.agent_name}' with timeout {timeout}s"
1316
+ )
1317
+
1318
+ out = agent.run(
1319
+ task=task,
1320
+ img=img,
1321
+ imgs=imgs,
1322
+ correct_answer=correct_answer,
1323
+ )
1324
+
1325
+ logger.debug(
1326
+ f"Agent '{agent.agent_name}' execution completed successfully"
1327
+ )
1328
+ return out
1329
+
1330
+ except Exception as e:
1331
+ error_msg = f"Agent execution failed: {str(e)}"
1332
+ logger.error(
1333
+ f"Execution error for agent '{agent.agent_name}': {error_msg}"
1334
+ )
1335
+ if self.traceback_enabled:
1336
+ logger.error(
1337
+ f"Traceback for agent '{agent.agent_name}':"
1338
+ )
1339
+ logger.error(traceback.format_exc())
1340
+ raise Exception(error_msg)
1341
+
1342
+ def remove_agent(self, tool_name: str) -> bool:
1343
+ """
1344
+ Remove an agent from the MCP server.
1345
+
1346
+ Args:
1347
+ tool_name: Name of the tool to remove
1348
+
1349
+ Returns:
1350
+ bool: True if agent was removed, False if not found
1351
+ """
1352
+ if tool_name in self.agents:
1353
+ # Stop and remove task queue if it exists and queue is enabled
1354
+ if self.queue_enabled and tool_name in self.task_queues:
1355
+ self.task_queues[tool_name].stop_workers()
1356
+ del self.task_queues[tool_name]
1357
+
1358
+ del self.agents[tool_name]
1359
+ del self.tool_configs[tool_name]
1360
+ logger.info(f"Removed agent tool '{tool_name}'")
1361
+ return True
1362
+ return False
1363
+
1364
+ def list_agents(self) -> List[str]:
1365
+ """
1366
+ Get a list of all registered agent tool names.
1367
+
1368
+ Returns:
1369
+ List[str]: List of tool names
1370
+ """
1371
+ agent_list = list(self.agents.keys())
1372
+ if self.verbose:
1373
+ logger.debug(
1374
+ f"Listing {len(agent_list)} registered agents: {agent_list}"
1375
+ )
1376
+ return agent_list
1377
+
1378
+ def get_agent_info(
1379
+ self, tool_name: str
1380
+ ) -> Optional[Dict[str, Any]]:
1381
+ """
1382
+ Get information about a specific agent tool.
1383
+
1384
+ Args:
1385
+ tool_name: Name of the tool
1386
+
1387
+ Returns:
1388
+ Dict containing agent information, or None if not found
1389
+ """
1390
+ if tool_name not in self.agents:
1391
+ if self.verbose:
1392
+ logger.debug(
1393
+ f"Requested info for non-existent agent tool '{tool_name}'"
1394
+ )
1395
+ return None
1396
+
1397
+ agent = self.agents[tool_name]
1398
+ config = self.tool_configs[tool_name]
1399
+
1400
+ info = {
1401
+ "tool_name": tool_name,
1402
+ "agent_name": agent.agent_name,
1403
+ "agent_description": agent.agent_description,
1404
+ "model_name": getattr(agent, "model_name", "Unknown"),
1405
+ "max_loops": getattr(agent, "max_loops", 1),
1406
+ "tool_description": config.tool_description,
1407
+ "timeout": config.timeout,
1408
+ "max_retries": config.max_retries,
1409
+ "verbose": config.verbose,
1410
+ "traceback_enabled": config.traceback_enabled,
1411
+ }
1412
+
1413
+ if self.verbose:
1414
+ logger.debug(
1415
+ f"Retrieved info for agent tool '{tool_name}': {info}"
1416
+ )
1417
+
1418
+ return info
1419
+
1420
+ def get_queue_stats(
1421
+ self, tool_name: Optional[str] = None
1422
+ ) -> Dict[str, Any]:
1423
+ """
1424
+ Get queue statistics for agents.
1425
+
1426
+ Args:
1427
+ tool_name: Optional specific agent name. If None, returns stats for all agents.
1428
+
1429
+ Returns:
1430
+ Dict containing queue statistics
1431
+ """
1432
+ if not self.queue_enabled:
1433
+ return {
1434
+ "success": False,
1435
+ "error": "Queue system is not enabled",
1436
+ "stats": {},
1437
+ }
1438
+
1439
+ try:
1440
+ if tool_name:
1441
+ if tool_name not in self.task_queues:
1442
+ return {
1443
+ "success": False,
1444
+ "error": f"Agent '{tool_name}' not found or has no queue",
1445
+ "stats": {},
1446
+ }
1447
+
1448
+ stats = self.task_queues[tool_name].get_stats()
1449
+ return {
1450
+ "success": True,
1451
+ "agent_name": tool_name,
1452
+ "stats": {
1453
+ "total_tasks": stats.total_tasks,
1454
+ "completed_tasks": stats.completed_tasks,
1455
+ "failed_tasks": stats.failed_tasks,
1456
+ "pending_tasks": stats.pending_tasks,
1457
+ "processing_tasks": stats.processing_tasks,
1458
+ "average_processing_time": stats.average_processing_time,
1459
+ "queue_size": stats.queue_size,
1460
+ "queue_status": self.task_queues[tool_name]
1461
+ .get_status()
1462
+ .value,
1463
+ },
1464
+ }
1465
+ else:
1466
+ # Get stats for all agents
1467
+ all_stats = {}
1468
+ for name, queue in self.task_queues.items():
1469
+ stats = queue.get_stats()
1470
+ all_stats[name] = {
1471
+ "total_tasks": stats.total_tasks,
1472
+ "completed_tasks": stats.completed_tasks,
1473
+ "failed_tasks": stats.failed_tasks,
1474
+ "pending_tasks": stats.pending_tasks,
1475
+ "processing_tasks": stats.processing_tasks,
1476
+ "average_processing_time": stats.average_processing_time,
1477
+ "queue_size": stats.queue_size,
1478
+ "queue_status": queue.get_status().value,
1479
+ }
1480
+
1481
+ return {
1482
+ "success": True,
1483
+ "stats": all_stats,
1484
+ "total_agents": len(all_stats),
1485
+ }
1486
+
1487
+ except Exception as e:
1488
+ error_msg = str(e)
1489
+ logger.error(f"Error getting queue stats: {error_msg}")
1490
+ return {
1491
+ "success": False,
1492
+ "error": error_msg,
1493
+ "stats": {},
1494
+ }
1495
+
1496
+ def pause_agent_queue(self, tool_name: str) -> bool:
1497
+ """
1498
+ Pause the task queue for a specific agent.
1499
+
1500
+ Args:
1501
+ tool_name: Name of the agent tool
1502
+
1503
+ Returns:
1504
+ bool: True if paused successfully, False if not found
1505
+ """
1506
+ if not self.queue_enabled:
1507
+ logger.warning("Queue system is not enabled")
1508
+ return False
1509
+
1510
+ if tool_name not in self.task_queues:
1511
+ logger.warning(
1512
+ f"Agent '{tool_name}' not found or has no queue"
1513
+ )
1514
+ return False
1515
+
1516
+ try:
1517
+ self.task_queues[tool_name].pause_workers()
1518
+ logger.info(f"Paused queue for agent '{tool_name}'")
1519
+ return True
1520
+ except Exception as e:
1521
+ logger.error(
1522
+ f"Error pausing queue for agent '{tool_name}': {e}"
1523
+ )
1524
+ return False
1525
+
1526
+ def resume_agent_queue(self, tool_name: str) -> bool:
1527
+ """
1528
+ Resume the task queue for a specific agent.
1529
+
1530
+ Args:
1531
+ tool_name: Name of the agent tool
1532
+
1533
+ Returns:
1534
+ bool: True if resumed successfully, False if not found
1535
+ """
1536
+ if not self.queue_enabled:
1537
+ logger.warning("Queue system is not enabled")
1538
+ return False
1539
+
1540
+ if tool_name not in self.task_queues:
1541
+ logger.warning(
1542
+ f"Agent '{tool_name}' not found or has no queue"
1543
+ )
1544
+ return False
1545
+
1546
+ try:
1547
+ self.task_queues[tool_name].resume_workers()
1548
+ logger.info(f"Resumed queue for agent '{tool_name}'")
1549
+ return True
1550
+ except Exception as e:
1551
+ logger.error(
1552
+ f"Error resuming queue for agent '{tool_name}': {e}"
1553
+ )
1554
+ return False
1555
+
1556
+ def clear_agent_queue(self, tool_name: str) -> int:
1557
+ """
1558
+ Clear all pending tasks from an agent's queue.
1559
+
1560
+ Args:
1561
+ tool_name: Name of the agent tool
1562
+
1563
+ Returns:
1564
+ int: Number of tasks cleared, -1 if error
1565
+ """
1566
+ if not self.queue_enabled:
1567
+ logger.warning("Queue system is not enabled")
1568
+ return -1
1569
+
1570
+ if tool_name not in self.task_queues:
1571
+ logger.warning(
1572
+ f"Agent '{tool_name}' not found or has no queue"
1573
+ )
1574
+ return -1
1575
+
1576
+ try:
1577
+ cleared_count = self.task_queues[tool_name].clear_queue()
1578
+ logger.info(
1579
+ f"Cleared {cleared_count} tasks from queue for agent '{tool_name}'"
1580
+ )
1581
+ return cleared_count
1582
+ except Exception as e:
1583
+ logger.error(
1584
+ f"Error clearing queue for agent '{tool_name}': {e}"
1585
+ )
1586
+ return -1
1587
+
1588
+ def get_task_status(
1589
+ self, tool_name: str, task_id: str
1590
+ ) -> Dict[str, Any]:
1591
+ """
1592
+ Get the status of a specific task.
1593
+
1594
+ Args:
1595
+ tool_name: Name of the agent tool
1596
+ task_id: ID of the task
1597
+
1598
+ Returns:
1599
+ Dict containing task status information
1600
+ """
1601
+ if not self.queue_enabled:
1602
+ return {
1603
+ "success": False,
1604
+ "error": "Queue system is not enabled",
1605
+ "task": None,
1606
+ }
1607
+
1608
+ if tool_name not in self.task_queues:
1609
+ return {
1610
+ "success": False,
1611
+ "error": f"Agent '{tool_name}' not found or has no queue",
1612
+ "task": None,
1613
+ }
1614
+
1615
+ try:
1616
+ task = self.task_queues[tool_name].get_task(task_id)
1617
+ if not task:
1618
+ return {
1619
+ "success": False,
1620
+ "error": f"Task '{task_id}' not found",
1621
+ "task": None,
1622
+ }
1623
+
1624
+ return {
1625
+ "success": True,
1626
+ "task": {
1627
+ "task_id": task.task_id,
1628
+ "status": task.status.value,
1629
+ "created_at": task.created_at,
1630
+ "result": task.result,
1631
+ "error": task.error,
1632
+ "retry_count": task.retry_count,
1633
+ "max_retries": task.max_retries,
1634
+ "priority": task.priority,
1635
+ },
1636
+ }
1637
+ except Exception as e:
1638
+ logger.error(f"Error getting task status: {e}")
1639
+ return {
1640
+ "success": False,
1641
+ "error": str(e),
1642
+ "task": None,
1643
+ }
1644
+
1645
+ def cancel_task(self, tool_name: str, task_id: str) -> bool:
1646
+ """
1647
+ Cancel a specific task.
1648
+
1649
+ Args:
1650
+ tool_name: Name of the agent tool
1651
+ task_id: ID of the task to cancel
1652
+
1653
+ Returns:
1654
+ bool: True if cancelled successfully, False otherwise
1655
+ """
1656
+ if not self.queue_enabled:
1657
+ logger.warning("Queue system is not enabled")
1658
+ return False
1659
+
1660
+ if tool_name not in self.task_queues:
1661
+ logger.warning(
1662
+ f"Agent '{tool_name}' not found or has no queue"
1663
+ )
1664
+ return False
1665
+
1666
+ try:
1667
+ success = self.task_queues[tool_name].cancel_task(task_id)
1668
+ if success:
1669
+ logger.info(
1670
+ f"Cancelled task '{task_id}' for agent '{tool_name}'"
1671
+ )
1672
+ else:
1673
+ logger.warning(
1674
+ f"Could not cancel task '{task_id}' for agent '{tool_name}'"
1675
+ )
1676
+ return success
1677
+ except Exception as e:
1678
+ logger.error(f"Error cancelling task '{task_id}': {e}")
1679
+ return False
1680
+
1681
+ def pause_all_queues(self) -> Dict[str, bool]:
1682
+ """
1683
+ Pause all agent queues.
1684
+
1685
+ Returns:
1686
+ Dict mapping agent names to success status
1687
+ """
1688
+ if not self.queue_enabled:
1689
+ logger.warning("Queue system is not enabled")
1690
+ return {}
1691
+
1692
+ results = {}
1693
+ for tool_name in self.task_queues.keys():
1694
+ results[tool_name] = self.pause_agent_queue(tool_name)
1695
+
1696
+ logger.info(
1697
+ f"Paused {sum(results.values())} out of {len(results)} agent queues"
1698
+ )
1699
+ return results
1700
+
1701
+ def resume_all_queues(self) -> Dict[str, bool]:
1702
+ """
1703
+ Resume all agent queues.
1704
+
1705
+ Returns:
1706
+ Dict mapping agent names to success status
1707
+ """
1708
+ if not self.queue_enabled:
1709
+ logger.warning("Queue system is not enabled")
1710
+ return {}
1711
+
1712
+ results = {}
1713
+ for tool_name in self.task_queues.keys():
1714
+ results[tool_name] = self.resume_agent_queue(tool_name)
1715
+
1716
+ logger.info(
1717
+ f"Resumed {sum(results.values())} out of {len(results)} agent queues"
1718
+ )
1719
+ return results
1720
+
1721
+ def clear_all_queues(self) -> Dict[str, int]:
1722
+ """
1723
+ Clear all agent queues.
1724
+
1725
+ Returns:
1726
+ Dict mapping agent names to number of tasks cleared
1727
+ """
1728
+ if not self.queue_enabled:
1729
+ logger.warning("Queue system is not enabled")
1730
+ return {}
1731
+
1732
+ results = {}
1733
+ total_cleared = 0
1734
+ for tool_name in self.task_queues.keys():
1735
+ cleared = self.clear_agent_queue(tool_name)
1736
+ results[tool_name] = cleared
1737
+ if cleared > 0:
1738
+ total_cleared += cleared
1739
+
1740
+ logger.info(
1741
+ f"Cleared {total_cleared} tasks from all agent queues"
1742
+ )
1743
+ return results
1744
+
1745
+ def _register_agent_discovery_tool(self) -> None:
1746
+ """
1747
+ Register the agent discovery tools that allow agents to learn about each other.
1748
+ """
1749
+
1750
+ @self.mcp_server.tool(
1751
+ name="discover_agents",
1752
+ description="Discover information about other agents in the cluster including their name, description, system prompt (truncated to 200 chars), and tags.",
1753
+ )
1754
+ def discover_agents(agent_name: str = None) -> Dict[str, Any]:
1755
+ """
1756
+ Discover information about agents in the cluster.
1757
+
1758
+ Args:
1759
+ agent_name: Optional specific agent name to get info for. If None, returns info for all agents.
1760
+
1761
+ Returns:
1762
+ Dict containing agent information for discovery
1763
+ """
1764
+ try:
1765
+ if agent_name:
1766
+ # Get specific agent info
1767
+ if agent_name not in self.agents:
1768
+ return {
1769
+ "success": False,
1770
+ "error": f"Agent '{agent_name}' not found",
1771
+ "agents": [],
1772
+ }
1773
+
1774
+ agent_info = self._get_agent_discovery_info(
1775
+ agent_name
1776
+ )
1777
+ return {
1778
+ "success": True,
1779
+ "agents": [agent_info] if agent_info else [],
1780
+ }
1781
+ else:
1782
+ # Get all agents info
1783
+ all_agents_info = []
1784
+ for tool_name in self.agents.keys():
1785
+ agent_info = self._get_agent_discovery_info(
1786
+ tool_name
1787
+ )
1788
+ if agent_info:
1789
+ all_agents_info.append(agent_info)
1790
+
1791
+ return {
1792
+ "success": True,
1793
+ "agents": all_agents_info,
1794
+ }
1795
+
1796
+ except Exception as e:
1797
+ error_msg = str(e)
1798
+ logger.error(
1799
+ f"Error in discover_agents tool: {error_msg}"
1800
+ )
1801
+ return {
1802
+ "success": False,
1803
+ "error": error_msg,
1804
+ "agents": [],
1805
+ }
1806
+
1807
+ @self.mcp_server.tool(
1808
+ name="get_agent_details",
1809
+ description="Get detailed information about a single agent by name including configuration, capabilities, and metadata.",
1810
+ )
1811
+ def get_agent_details(agent_name: str) -> Dict[str, Any]:
1812
+ """
1813
+ Get detailed information about a specific agent.
1814
+
1815
+ Args:
1816
+ agent_name: Name of the agent to get information for.
1817
+
1818
+ Returns:
1819
+ Dict containing detailed agent information
1820
+ """
1821
+ try:
1822
+ if agent_name not in self.agents:
1823
+ return {
1824
+ "success": False,
1825
+ "error": f"Agent '{agent_name}' not found",
1826
+ "agent_info": None,
1827
+ }
1828
+
1829
+ agent_info = self.get_agent_info(agent_name)
1830
+ discovery_info = self._get_agent_discovery_info(
1831
+ agent_name
1832
+ )
1833
+
1834
+ return {
1835
+ "success": True,
1836
+ "agent_info": agent_info,
1837
+ "discovery_info": discovery_info,
1838
+ }
1839
+
1840
+ except Exception as e:
1841
+ error_msg = str(e)
1842
+ logger.error(
1843
+ f"Error in get_agent_details tool: {error_msg}"
1844
+ )
1845
+ return {
1846
+ "success": False,
1847
+ "error": error_msg,
1848
+ "agent_info": None,
1849
+ }
1850
+
1851
+ @self.mcp_server.tool(
1852
+ name="get_agents_info",
1853
+ description="Get detailed information about multiple agents by providing a list of agent names.",
1854
+ )
1855
+ def get_agents_info(agent_names: List[str]) -> Dict[str, Any]:
1856
+ """
1857
+ Get detailed information about multiple agents.
1858
+
1859
+ Args:
1860
+ agent_names: List of agent names to get information for.
1861
+
1862
+ Returns:
1863
+ Dict containing detailed information for all requested agents
1864
+ """
1865
+ try:
1866
+ if not agent_names:
1867
+ return {
1868
+ "success": False,
1869
+ "error": "No agent names provided",
1870
+ "agents_info": [],
1871
+ }
1872
+
1873
+ agents_info = []
1874
+ not_found = []
1875
+
1876
+ for agent_name in agent_names:
1877
+ if agent_name in self.agents:
1878
+ agent_info = self.get_agent_info(agent_name)
1879
+ discovery_info = (
1880
+ self._get_agent_discovery_info(agent_name)
1881
+ )
1882
+ agents_info.append(
1883
+ {
1884
+ "agent_name": agent_name,
1885
+ "agent_info": agent_info,
1886
+ "discovery_info": discovery_info,
1887
+ }
1888
+ )
1889
+ else:
1890
+ not_found.append(agent_name)
1891
+
1892
+ return {
1893
+ "success": True,
1894
+ "agents_info": agents_info,
1895
+ "not_found": not_found,
1896
+ "total_found": len(agents_info),
1897
+ "total_requested": len(agent_names),
1898
+ }
1899
+
1900
+ except Exception as e:
1901
+ error_msg = str(e)
1902
+ logger.error(
1903
+ f"Error in get_agents_info tool: {error_msg}"
1904
+ )
1905
+ return {
1906
+ "success": False,
1907
+ "error": error_msg,
1908
+ "agents_info": [],
1909
+ }
1910
+
1911
+ @self.mcp_server.tool(
1912
+ name="list_agents",
1913
+ description="Get a simple list of all available agent names in the cluster.",
1914
+ )
1915
+ def list_agents() -> Dict[str, Any]:
1916
+ """
1917
+ Get a list of all available agent names.
1918
+
1919
+ Returns:
1920
+ Dict containing the list of agent names
1921
+ """
1922
+ try:
1923
+ agent_names = self.list_agents()
1924
+ return {
1925
+ "success": True,
1926
+ "agent_names": agent_names,
1927
+ "total_count": len(agent_names),
1928
+ }
1929
+
1930
+ except Exception as e:
1931
+ error_msg = str(e)
1932
+ logger.error(
1933
+ f"Error in list_agents tool: {error_msg}"
1934
+ )
1935
+ return {
1936
+ "success": False,
1937
+ "error": error_msg,
1938
+ "agent_names": [],
1939
+ }
1940
+
1941
+ @self.mcp_server.tool(
1942
+ name="search_agents",
1943
+ description="Search for agents by name, description, tags, or capabilities using keyword matching.",
1944
+ )
1945
+ def search_agents(
1946
+ query: str, search_fields: List[str] = None
1947
+ ) -> Dict[str, Any]:
1948
+ """
1949
+ Search for agents using keyword matching.
1950
+
1951
+ Args:
1952
+ query: Search query string
1953
+ search_fields: Optional list of fields to search in (name, description, tags, capabilities).
1954
+ If None, searches all fields.
1955
+
1956
+ Returns:
1957
+ Dict containing matching agents
1958
+ """
1959
+ try:
1960
+ if not query:
1961
+ return {
1962
+ "success": False,
1963
+ "error": "No search query provided",
1964
+ "matching_agents": [],
1965
+ }
1966
+
1967
+ # Default search fields
1968
+ if search_fields is None:
1969
+ search_fields = [
1970
+ "name",
1971
+ "description",
1972
+ "tags",
1973
+ "capabilities",
1974
+ ]
1975
+
1976
+ query_lower = query.lower()
1977
+ matching_agents = []
1978
+
1979
+ for tool_name in self.agents.keys():
1980
+ discovery_info = self._get_agent_discovery_info(
1981
+ tool_name
1982
+ )
1983
+ if not discovery_info:
1984
+ continue
1985
+
1986
+ match_found = False
1987
+
1988
+ # Search in specified fields
1989
+ for field in search_fields:
1990
+ if (
1991
+ field == "name"
1992
+ and query_lower
1993
+ in discovery_info.get(
1994
+ "agent_name", ""
1995
+ ).lower()
1996
+ ):
1997
+ match_found = True
1998
+ break
1999
+ elif (
2000
+ field == "description"
2001
+ and query_lower
2002
+ in discovery_info.get(
2003
+ "description", ""
2004
+ ).lower()
2005
+ ):
2006
+ match_found = True
2007
+ break
2008
+ elif field == "tags":
2009
+ tags = discovery_info.get("tags", [])
2010
+ if any(
2011
+ query_lower in tag.lower()
2012
+ for tag in tags
2013
+ ):
2014
+ match_found = True
2015
+ break
2016
+ elif field == "capabilities":
2017
+ capabilities = discovery_info.get(
2018
+ "capabilities", []
2019
+ )
2020
+ if any(
2021
+ query_lower in capability.lower()
2022
+ for capability in capabilities
2023
+ ):
2024
+ match_found = True
2025
+ break
2026
+
2027
+ if match_found:
2028
+ matching_agents.append(discovery_info)
2029
+
2030
+ return {
2031
+ "success": True,
2032
+ "matching_agents": matching_agents,
2033
+ "total_matches": len(matching_agents),
2034
+ "query": query,
2035
+ "search_fields": search_fields,
2036
+ }
2037
+
2038
+ except Exception as e:
2039
+ error_msg = str(e)
2040
+ logger.error(
2041
+ f"Error in search_agents tool: {error_msg}"
2042
+ )
2043
+ return {
2044
+ "success": False,
2045
+ "error": error_msg,
2046
+ "matching_agents": [],
2047
+ }
2048
+
2049
+ @self.mcp_server.tool(
2050
+ name="get_server_info",
2051
+ description="Get comprehensive server information including metadata, configuration, tool details, queue stats, and network status.",
2052
+ )
2053
+ def get_server_info_tool() -> Dict[str, Any]:
2054
+ """
2055
+ Get comprehensive information about the MCP server and registered tools.
2056
+
2057
+ Returns:
2058
+ Dict containing server information with the following fields:
2059
+ - server_name: Name of the server
2060
+ - description: Server description
2061
+ - total_tools/total_agents: Total number of agents registered
2062
+ - tools/agent_names: List of all agent names
2063
+ - created_at: Unix timestamp when server was created
2064
+ - created_at_iso: ISO formatted creation time
2065
+ - uptime_seconds: Server uptime in seconds
2066
+ - host: Server host address
2067
+ - port: Server port number
2068
+ - transport: Transport protocol used
2069
+ - log_level: Logging level
2070
+ - queue_enabled: Whether queue system is enabled
2071
+ - persistence_enabled: Whether persistence mode is enabled
2072
+ - network_monitoring_enabled: Whether network monitoring is enabled
2073
+ - persistence: Detailed persistence status
2074
+ - network: Detailed network status
2075
+ - tool_details: Detailed information about each agent tool
2076
+ - queue_config: Queue configuration (if queue enabled)
2077
+ - queue_stats: Queue statistics for each agent (if queue enabled)
2078
+ """
2079
+ try:
2080
+ server_info = self.get_server_info()
2081
+ return {
2082
+ "success": True,
2083
+ "server_info": server_info,
2084
+ }
2085
+ except Exception as e:
2086
+ error_msg = str(e)
2087
+ logger.error(
2088
+ f"Error in get_server_info tool: {error_msg}"
2089
+ )
2090
+ return {
2091
+ "success": False,
2092
+ "error": error_msg,
2093
+ "server_info": None,
2094
+ }
2095
+
2096
+ def _register_queue_management_tools(self) -> None:
2097
+ """
2098
+ Register queue management tools for the MCP server.
2099
+ """
2100
+
2101
+ @self.mcp_server.tool(
2102
+ name="get_queue_stats",
2103
+ description="Get queue statistics for agents including task counts, processing times, and queue status.",
2104
+ )
2105
+ def get_queue_stats(agent_name: str = None) -> Dict[str, Any]:
2106
+ """
2107
+ Get queue statistics for agents.
2108
+
2109
+ Args:
2110
+ agent_name: Optional specific agent name. If None, returns stats for all agents.
2111
+
2112
+ Returns:
2113
+ Dict containing queue statistics
2114
+ """
2115
+ return self.get_queue_stats(agent_name)
2116
+
2117
+ @self.mcp_server.tool(
2118
+ name="pause_agent_queue",
2119
+ description="Pause the task queue for a specific agent.",
2120
+ )
2121
+ def pause_agent_queue(agent_name: str) -> Dict[str, Any]:
2122
+ """
2123
+ Pause the task queue for a specific agent.
2124
+
2125
+ Args:
2126
+ agent_name: Name of the agent tool
2127
+
2128
+ Returns:
2129
+ Dict containing success status
2130
+ """
2131
+ success = self.pause_agent_queue(agent_name)
2132
+ return {
2133
+ "success": success,
2134
+ "message": f"Queue for agent '{agent_name}' {'paused' if success else 'not found or already paused'}",
2135
+ }
2136
+
2137
+ @self.mcp_server.tool(
2138
+ name="resume_agent_queue",
2139
+ description="Resume the task queue for a specific agent.",
2140
+ )
2141
+ def resume_agent_queue(agent_name: str) -> Dict[str, Any]:
2142
+ """
2143
+ Resume the task queue for a specific agent.
2144
+
2145
+ Args:
2146
+ agent_name: Name of the agent tool
2147
+
2148
+ Returns:
2149
+ Dict containing success status
2150
+ """
2151
+ success = self.resume_agent_queue(agent_name)
2152
+ return {
2153
+ "success": success,
2154
+ "message": f"Queue for agent '{agent_name}' {'resumed' if success else 'not found or already running'}",
2155
+ }
2156
+
2157
+ @self.mcp_server.tool(
2158
+ name="clear_agent_queue",
2159
+ description="Clear all pending tasks from an agent's queue.",
2160
+ )
2161
+ def clear_agent_queue(agent_name: str) -> Dict[str, Any]:
2162
+ """
2163
+ Clear all pending tasks from an agent's queue.
2164
+
2165
+ Args:
2166
+ agent_name: Name of the agent tool
2167
+
2168
+ Returns:
2169
+ Dict containing number of tasks cleared
2170
+ """
2171
+ cleared_count = self.clear_agent_queue(agent_name)
2172
+ return {
2173
+ "success": cleared_count >= 0,
2174
+ "cleared_tasks": cleared_count,
2175
+ "message": (
2176
+ f"Cleared {cleared_count} tasks from queue for agent '{agent_name}'"
2177
+ if cleared_count >= 0
2178
+ else f"Failed to clear queue for agent '{agent_name}'"
2179
+ ),
2180
+ }
2181
+
2182
+ @self.mcp_server.tool(
2183
+ name="get_task_status",
2184
+ description="Get the status of a specific task by task ID.",
2185
+ )
2186
+ def get_task_status(
2187
+ agent_name: str, task_id: str
2188
+ ) -> Dict[str, Any]:
2189
+ """
2190
+ Get the status of a specific task.
2191
+
2192
+ Args:
2193
+ agent_name: Name of the agent tool
2194
+ task_id: ID of the task
2195
+
2196
+ Returns:
2197
+ Dict containing task status information
2198
+ """
2199
+ return self.get_task_status(agent_name, task_id)
2200
+
2201
+ @self.mcp_server.tool(
2202
+ name="cancel_task",
2203
+ description="Cancel a specific task by task ID.",
2204
+ )
2205
+ def cancel_task(
2206
+ agent_name: str, task_id: str
2207
+ ) -> Dict[str, Any]:
2208
+ """
2209
+ Cancel a specific task.
2210
+
2211
+ Args:
2212
+ agent_name: Name of the agent tool
2213
+ task_id: ID of the task to cancel
2214
+
2215
+ Returns:
2216
+ Dict containing success status
2217
+ """
2218
+ success = self.cancel_task(agent_name, task_id)
2219
+ return {
2220
+ "success": success,
2221
+ "message": f"Task '{task_id}' {'cancelled' if success else 'not found or already processed'}",
2222
+ }
2223
+
2224
+ @self.mcp_server.tool(
2225
+ name="pause_all_queues",
2226
+ description="Pause all agent queues.",
2227
+ )
2228
+ def pause_all_queues() -> Dict[str, Any]:
2229
+ """
2230
+ Pause all agent queues.
2231
+
2232
+ Returns:
2233
+ Dict containing results for each agent
2234
+ """
2235
+ results = self.pause_all_queues()
2236
+ return {
2237
+ "success": True,
2238
+ "results": results,
2239
+ "total_agents": len(results),
2240
+ "successful_pauses": sum(results.values()),
2241
+ }
2242
+
2243
+ @self.mcp_server.tool(
2244
+ name="resume_all_queues",
2245
+ description="Resume all agent queues.",
2246
+ )
2247
+ def resume_all_queues() -> Dict[str, Any]:
2248
+ """
2249
+ Resume all agent queues.
2250
+
2251
+ Returns:
2252
+ Dict containing results for each agent
2253
+ """
2254
+ results = self.resume_all_queues()
2255
+ return {
2256
+ "success": True,
2257
+ "results": results,
2258
+ "total_agents": len(results),
2259
+ "successful_resumes": sum(results.values()),
2260
+ }
2261
+
2262
+ @self.mcp_server.tool(
2263
+ name="clear_all_queues",
2264
+ description="Clear all agent queues.",
2265
+ )
2266
+ def clear_all_queues() -> Dict[str, Any]:
2267
+ """
2268
+ Clear all agent queues.
2269
+
2270
+ Returns:
2271
+ Dict containing results for each agent
2272
+ """
2273
+ results = self.clear_all_queues()
2274
+ total_cleared = sum(results.values())
2275
+ return {
2276
+ "success": True,
2277
+ "results": results,
2278
+ "total_agents": len(results),
2279
+ "total_cleared": total_cleared,
2280
+ }
2281
+
2282
+ def _get_agent_discovery_info(
2283
+ self, tool_name: str
2284
+ ) -> Optional[Dict[str, Any]]:
2285
+ """
2286
+ Get discovery information for a specific agent.
2287
+
2288
+ Args:
2289
+ tool_name: Name of the agent tool
2290
+
2291
+ Returns:
2292
+ Dict containing agent discovery information, or None if not found
2293
+ """
2294
+ if tool_name not in self.agents:
2295
+ return None
2296
+
2297
+ agent = self.agents[tool_name]
2298
+
2299
+ # Get system prompt and truncate to 200 characters
2300
+ system_prompt = getattr(agent, "system_prompt", "")
2301
+ short_system_prompt = (
2302
+ system_prompt[:200] + "..."
2303
+ if len(system_prompt) > 200
2304
+ else system_prompt
2305
+ )
2306
+
2307
+ # Get tags (if available)
2308
+ tags = getattr(agent, "tags", [])
2309
+ if not tags:
2310
+ tags = []
2311
+
2312
+ # Get capabilities (if available)
2313
+ capabilities = getattr(agent, "capabilities", [])
2314
+ if not capabilities:
2315
+ capabilities = []
2316
+
2317
+ # Get role (if available)
2318
+ role = getattr(agent, "role", "worker")
2319
+
2320
+ # Get model name
2321
+ model_name = getattr(agent, "model_name", "Unknown")
2322
+
2323
+ info = {
2324
+ "tool_name": tool_name,
2325
+ "agent_name": agent.agent_name,
2326
+ "description": agent.agent_description
2327
+ or "No description available",
2328
+ "short_system_prompt": short_system_prompt,
2329
+ "tags": tags,
2330
+ "capabilities": capabilities,
2331
+ "role": role,
2332
+ "model_name": model_name,
2333
+ "max_loops": getattr(agent, "max_loops", 1),
2334
+ "temperature": getattr(agent, "temperature", 0.5),
2335
+ "max_tokens": getattr(agent, "max_tokens", 4096),
2336
+ }
2337
+
2338
+ if self.verbose:
2339
+ logger.debug(
2340
+ f"Retrieved discovery info for agent '{tool_name}': {info}"
2341
+ )
2342
+
2343
+ return info
2344
+
2345
+ def start_server(self) -> None:
2346
+ """
2347
+ Start the MCP server.
2348
+
2349
+ Args:
2350
+ host: Host to bind the server to
2351
+ port: Port to bind the server to
2352
+ """
2353
+ logger.info(
2354
+ f"Starting MCP server '{self.server_name}' on {self.host}:{self.port}\n"
2355
+ f"Transport: {self.transport}\n"
2356
+ f"Log level: {self.log_level}\n"
2357
+ f"Verbose mode: {self.verbose}\n"
2358
+ f"Traceback enabled: {self.traceback_enabled}\n"
2359
+ f"Queue enabled: {self.queue_enabled}\n"
2360
+ f"Available tools: {self.list_agents()}"
2361
+ )
2362
+
2363
+ if self.verbose:
2364
+ logger.debug(
2365
+ "Server configuration:\n"
2366
+ f" - Server name: {self.server_name}\n"
2367
+ f" - Host: {self.host}\n"
2368
+ f" - Port: {self.port}\n"
2369
+ f" - Transport: {self.transport}\n"
2370
+ f" - Queue enabled: {self.queue_enabled}\n"
2371
+ f" - Total agents: {len(self.agents)}"
2372
+ )
2373
+ for tool_name, config in self.tool_configs.items():
2374
+ logger.debug(
2375
+ f" - Tool '{tool_name}': timeout={config.timeout}s, verbose={config.verbose}, traceback={config.traceback_enabled}"
2376
+ )
2377
+
2378
+ if self.queue_enabled:
2379
+ logger.debug(
2380
+ f" - Max workers per agent: {self.max_workers_per_agent}"
2381
+ )
2382
+ logger.debug(
2383
+ f" - Max queue size per agent: {self.max_queue_size_per_agent}"
2384
+ )
2385
+ logger.debug(
2386
+ f" - Processing timeout: {self.processing_timeout}s"
2387
+ )
2388
+ logger.debug(f" - Retry delay: {self.retry_delay}s")
2389
+
2390
+ try:
2391
+ self.mcp_server.run(transport=self.transport)
2392
+ except KeyboardInterrupt:
2393
+ logger.info("Server interrupted by user")
2394
+ finally:
2395
+ # Clean up queues when server stops
2396
+ if self.queue_enabled:
2397
+ logger.info("Stopping all agent queues...")
2398
+ for tool_name in list(self.task_queues.keys()):
2399
+ try:
2400
+ self.task_queues[tool_name].stop_workers()
2401
+ logger.debug(
2402
+ f"Stopped queue for agent '{tool_name}'"
2403
+ )
2404
+ except Exception as e:
2405
+ logger.error(
2406
+ f"Error stopping queue for agent '{tool_name}': {e}"
2407
+ )
2408
+
2409
+ logger.info(
2410
+ f"MCP Server '{self.server_name}' is ready with {len(self.agents)} tools"
2411
+ )
2412
+ logger.info(
2413
+ f"Tools available: {', '.join(self.list_agents())}"
2414
+ )
2415
+
2416
+ def run(self) -> None:
2417
+ """
2418
+ Run the MCP server with optional persistence.
2419
+
2420
+ If persistence is enabled, the server will automatically restart
2421
+ when stopped, up to max_restart_attempts times. This includes
2422
+ a failsafe mechanism to prevent infinite restart loops.
2423
+ """
2424
+ if not self._persistence_enabled:
2425
+ # Standard run without persistence
2426
+ self.start_server()
2427
+ return
2428
+
2429
+ # Persistence-enabled run
2430
+ logger.info(
2431
+ f"Starting AOP server with persistence enabled (max restarts: {self.max_restart_attempts})"
2432
+ )
2433
+
2434
+ while (
2435
+ not self._shutdown_requested
2436
+ and self._restart_count <= self.max_restart_attempts
2437
+ ):
2438
+ try:
2439
+ if self._restart_count > 0:
2440
+ logger.info(
2441
+ f"Restarting server (attempt {self._restart_count}/{self.max_restart_attempts})"
2442
+ )
2443
+ # Wait before restarting
2444
+ time.sleep(self.restart_delay)
2445
+
2446
+ # Reset restart count on successful start
2447
+ self._restart_count = 0
2448
+ self.start_server()
2449
+
2450
+ except KeyboardInterrupt:
2451
+ if (
2452
+ self._persistence_enabled
2453
+ and not self._shutdown_requested
2454
+ ):
2455
+ logger.warning(
2456
+ "Server interrupted by user, but persistence is enabled. Restarting..."
2457
+ )
2458
+ self._restart_count += 1
2459
+ continue
2460
+ else:
2461
+ logger.info("Server shutdown requested by user")
2462
+ break
2463
+
2464
+ except Exception as e:
2465
+ if (
2466
+ self._persistence_enabled
2467
+ and not self._shutdown_requested
2468
+ ):
2469
+ # Check if it's a network error
2470
+ if self._is_network_error(e):
2471
+ logger.warning(
2472
+ "🌐 Network error detected, attempting reconnection..."
2473
+ )
2474
+ if self._handle_network_error(e):
2475
+ # Network retry successful, continue with restart
2476
+ self._restart_count += 1
2477
+ continue
2478
+ else:
2479
+ # Network retry failed, give up
2480
+ logger.critical(
2481
+ "💀 Network reconnection failed permanently"
2482
+ )
2483
+ break
2484
+ else:
2485
+ # Non-network error, use standard restart logic
2486
+ logger.error(
2487
+ f"Server crashed with error: {e}"
2488
+ )
2489
+ self._restart_count += 1
2490
+
2491
+ if (
2492
+ self._restart_count
2493
+ > self.max_restart_attempts
2494
+ ):
2495
+ logger.critical(
2496
+ f"Maximum restart attempts ({self.max_restart_attempts}) exceeded. Shutting down permanently."
2497
+ )
2498
+ break
2499
+ else:
2500
+ logger.info(
2501
+ f"Will restart in {self.restart_delay} seconds..."
2502
+ )
2503
+ continue
2504
+ else:
2505
+ # Check if it's a network error even without persistence
2506
+ if self._is_network_error(e):
2507
+ logger.error(
2508
+ "🌐 Network error detected but persistence is disabled"
2509
+ )
2510
+ if self.network_monitoring:
2511
+ logger.info(
2512
+ "🔄 Attempting network reconnection..."
2513
+ )
2514
+ if self._handle_network_error(e):
2515
+ # Try to start server again after network recovery
2516
+ try:
2517
+ self.start_server()
2518
+ return
2519
+ except Exception as retry_error:
2520
+ logger.error(
2521
+ f"Server failed after network recovery: {retry_error}"
2522
+ )
2523
+ raise
2524
+ else:
2525
+ logger.critical(
2526
+ "💀 Network reconnection failed"
2527
+ )
2528
+ raise
2529
+ else:
2530
+ logger.error(
2531
+ "Network monitoring is disabled, cannot retry"
2532
+ )
2533
+ raise
2534
+ else:
2535
+ logger.error(
2536
+ f"Server failed and persistence is disabled: {e}"
2537
+ )
2538
+ raise
2539
+
2540
+ if self._restart_count > self.max_restart_attempts:
2541
+ logger.critical(
2542
+ "Server failed permanently due to exceeding maximum restart attempts"
2543
+ )
2544
+ elif self._shutdown_requested:
2545
+ logger.info("Server shutdown completed as requested")
2546
+ else:
2547
+ logger.info("Server stopped normally")
2548
+
2549
+ def _is_network_error(self, error: Exception) -> bool:
2550
+ """
2551
+ Check if an error is network-related.
2552
+
2553
+ Args:
2554
+ error: The exception to check
2555
+
2556
+ Returns:
2557
+ bool: True if the error is network-related
2558
+ """
2559
+ network_errors = (
2560
+ ConnectionError,
2561
+ ConnectionRefusedError,
2562
+ ConnectionResetError,
2563
+ ConnectionAbortedError,
2564
+ TimeoutError,
2565
+ socket.gaierror,
2566
+ socket.timeout,
2567
+ OSError,
2568
+ )
2569
+
2570
+ # Check if it's a direct network error
2571
+ if isinstance(error, network_errors):
2572
+ return True
2573
+
2574
+ # Check error message for network-related keywords
2575
+ error_msg = str(error).lower()
2576
+ network_keywords = [
2577
+ "connection refused",
2578
+ "connection reset",
2579
+ "connection aborted",
2580
+ "network is unreachable",
2581
+ "no route to host",
2582
+ "timeout",
2583
+ "socket",
2584
+ "network",
2585
+ "connection",
2586
+ "refused",
2587
+ "reset",
2588
+ "aborted",
2589
+ "unreachable",
2590
+ "timeout",
2591
+ ]
2592
+
2593
+ return any(
2594
+ keyword in error_msg for keyword in network_keywords
2595
+ )
2596
+
2597
+ def _get_network_error_message(
2598
+ self, error: Exception, attempt: int
2599
+ ) -> str:
2600
+ """
2601
+ Get a custom error message for network-related errors.
2602
+
2603
+ Args:
2604
+ error: The network error that occurred
2605
+ attempt: Current retry attempt number
2606
+
2607
+ Returns:
2608
+ str: Custom error message
2609
+ """
2610
+ error_type = type(error).__name__
2611
+ error_msg = str(error)
2612
+
2613
+ if isinstance(error, ConnectionRefusedError):
2614
+ return f"🌐 NETWORK ERROR: Connection refused to {self.host}:{self.port} (attempt {attempt}/{self.max_network_retries})"
2615
+ elif isinstance(error, ConnectionResetError):
2616
+ return f"🌐 NETWORK ERROR: Connection was reset by remote host (attempt {attempt}/{self.max_network_retries})"
2617
+ elif isinstance(error, ConnectionAbortedError):
2618
+ return f"🌐 NETWORK ERROR: Connection was aborted (attempt {attempt}/{self.max_network_retries})"
2619
+ elif isinstance(error, TimeoutError):
2620
+ return f"🌐 NETWORK ERROR: Connection timeout after {self.network_timeout}s (attempt {attempt}/{self.max_network_retries})"
2621
+ elif isinstance(error, socket.gaierror):
2622
+ return f"🌐 NETWORK ERROR: Host resolution failed for {self.host} (attempt {attempt}/{self.max_network_retries})"
2623
+ elif isinstance(error, OSError):
2624
+ return f"🌐 NETWORK ERROR: OS-level network error - {error_msg} (attempt {attempt}/{self.max_network_retries})"
2625
+ else:
2626
+ return f"🌐 NETWORK ERROR: {error_type} - {error_msg} (attempt {attempt}/{self.max_network_retries})"
2627
+
2628
+ def _test_network_connectivity(self) -> bool:
2629
+ """
2630
+ Test network connectivity to the server host and port.
2631
+
2632
+ Returns:
2633
+ bool: True if network is reachable, False otherwise
2634
+ """
2635
+ try:
2636
+ # Test if we can resolve the host
2637
+ socket.gethostbyname(self.host)
2638
+
2639
+ # Test if we can connect to the port
2640
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
2641
+ sock.settimeout(self.network_timeout)
2642
+ result = sock.connect_ex((self.host, self.port))
2643
+ sock.close()
2644
+
2645
+ return result == 0
2646
+ except Exception as e:
2647
+ if self.verbose:
2648
+ logger.debug(f"Network connectivity test failed: {e}")
2649
+ return False
2650
+
2651
+ def _handle_network_error(self, error: Exception) -> bool:
2652
+ """
2653
+ Handle network errors with retry logic.
2654
+
2655
+ Args:
2656
+ error: The network error that occurred
2657
+
2658
+ Returns:
2659
+ bool: True if should retry, False if should give up
2660
+ """
2661
+ if not self.network_monitoring:
2662
+ return False
2663
+
2664
+ self._network_retry_count += 1
2665
+ self._last_network_error = error
2666
+ self._network_connected = False
2667
+
2668
+ # Get custom error message
2669
+ error_msg = self._get_network_error_message(
2670
+ error, self._network_retry_count
2671
+ )
2672
+ logger.error(error_msg)
2673
+
2674
+ # Check if we should retry
2675
+ if self._network_retry_count <= self.max_network_retries:
2676
+ logger.warning(
2677
+ f"🔄 Attempting to reconnect in {self.network_retry_delay} seconds..."
2678
+ )
2679
+ logger.info(
2680
+ f"📊 Network retry {self._network_retry_count}/{self.max_network_retries}"
2681
+ )
2682
+
2683
+ # Wait before retry
2684
+ time.sleep(self.network_retry_delay)
2685
+
2686
+ # Test connectivity before retry
2687
+ if self._test_network_connectivity():
2688
+ logger.info("✅ Network connectivity restored!")
2689
+ self._network_connected = True
2690
+ self._network_retry_count = (
2691
+ 0 # Reset on successful test
2692
+ )
2693
+ return True
2694
+ else:
2695
+ logger.warning(
2696
+ "❌ Network connectivity test failed, will retry..."
2697
+ )
2698
+ return True
2699
+ else:
2700
+ logger.critical(
2701
+ f"💀 Maximum network retry attempts ({self.max_network_retries}) exceeded!"
2702
+ )
2703
+ logger.critical(
2704
+ "🚫 Giving up on network reconnection. Server will shut down."
2705
+ )
2706
+ return False
2707
+
2708
+ def get_network_status(self) -> Dict[str, Any]:
2709
+ """
2710
+ Get current network status and statistics.
2711
+
2712
+ Returns:
2713
+ Dict containing network status information
2714
+ """
2715
+ return {
2716
+ "network_monitoring_enabled": self.network_monitoring,
2717
+ "network_connected": self._network_connected,
2718
+ "network_retry_count": self._network_retry_count,
2719
+ "max_network_retries": self.max_network_retries,
2720
+ "network_retry_delay": self.network_retry_delay,
2721
+ "network_timeout": self.network_timeout,
2722
+ "last_network_error": (
2723
+ str(self._last_network_error)
2724
+ if self._last_network_error
2725
+ else None
2726
+ ),
2727
+ "remaining_network_retries": max(
2728
+ 0,
2729
+ self.max_network_retries - self._network_retry_count,
2730
+ ),
2731
+ "host": self.host,
2732
+ "port": self.port,
2733
+ }
2734
+
2735
+ def reset_network_retry_count(self) -> None:
2736
+ """
2737
+ Reset the network retry counter.
2738
+
2739
+ This can be useful if you want to give the server a fresh
2740
+ set of network retry attempts.
2741
+ """
2742
+ self._network_retry_count = 0
2743
+ self._last_network_error = None
2744
+ self._network_connected = True
2745
+ logger.info("Network retry counter reset")
2746
+
2747
+ def enable_persistence(self) -> None:
2748
+ """
2749
+ Enable persistence mode for the server.
2750
+
2751
+ This allows the server to automatically restart when stopped,
2752
+ up to the maximum number of restart attempts.
2753
+ """
2754
+ self._persistence_enabled = True
2755
+ logger.info("Persistence mode enabled")
2756
+
2757
+ def disable_persistence(self) -> None:
2758
+ """
2759
+ Disable persistence mode for the server.
2760
+
2761
+ This will allow the server to shut down normally without
2762
+ automatic restarts.
2763
+ """
2764
+ self._persistence_enabled = False
2765
+ self._shutdown_requested = True
2766
+ logger.info(
2767
+ "Persistence mode disabled - server will shut down on next stop"
2768
+ )
2769
+
2770
+ def request_shutdown(self) -> None:
2771
+ """
2772
+ Request a graceful shutdown of the server.
2773
+
2774
+ If persistence is enabled, this will prevent automatic restarts
2775
+ and allow the server to shut down normally.
2776
+ """
2777
+ self._shutdown_requested = True
2778
+ logger.info(
2779
+ "Shutdown requested - server will stop after current operations complete"
2780
+ )
2781
+
2782
+ def get_persistence_status(self) -> Dict[str, Any]:
2783
+ """
2784
+ Get the current persistence status and statistics.
2785
+
2786
+ Returns:
2787
+ Dict containing persistence configuration and status
2788
+ """
2789
+ return {
2790
+ "persistence_enabled": self._persistence_enabled,
2791
+ "shutdown_requested": self._shutdown_requested,
2792
+ "restart_count": self._restart_count,
2793
+ "max_restart_attempts": self.max_restart_attempts,
2794
+ "restart_delay": self.restart_delay,
2795
+ "remaining_restarts": max(
2796
+ 0, self.max_restart_attempts - self._restart_count
2797
+ ),
2798
+ }
2799
+
2800
+ def reset_restart_count(self) -> None:
2801
+ """
2802
+ Reset the restart counter.
2803
+
2804
+ This can be useful if you want to give the server a fresh
2805
+ set of restart attempts.
2806
+ """
2807
+ self._restart_count = 0
2808
+ logger.info("Restart counter reset")
2809
+
2810
+ def get_server_info(self) -> Dict[str, Any]:
2811
+ """
2812
+ Get information about the MCP server and registered tools.
2813
+
2814
+ Returns:
2815
+ Dict containing server information including metadata, configuration,
2816
+ and tool details
2817
+ """
2818
+ info = {
2819
+ "server_name": self.server_name,
2820
+ "description": self.description,
2821
+ "total_tools": len(self.agents),
2822
+ "total_agents": len(
2823
+ self.agents
2824
+ ), # Alias for compatibility
2825
+ "tools": self.list_agents(),
2826
+ "agent_names": self.list_agents(), # Alias for compatibility
2827
+ "created_at": self._created_at,
2828
+ "created_at_iso": time.strftime(
2829
+ "%Y-%m-%d %H:%M:%S", time.localtime(self._created_at)
2830
+ ),
2831
+ "uptime_seconds": time.time() - self._created_at,
2832
+ "verbose": self.verbose,
2833
+ "traceback_enabled": self.traceback_enabled,
2834
+ "log_level": self.log_level,
2835
+ "transport": self.transport,
2836
+ "host": self.host,
2837
+ "port": self.port,
2838
+ "queue_enabled": self.queue_enabled,
2839
+ "persistence_enabled": self._persistence_enabled, # Top-level for compatibility
2840
+ "network_monitoring_enabled": self.network_monitoring, # Top-level for compatibility
2841
+ "persistence": self.get_persistence_status(),
2842
+ "network": self.get_network_status(),
2843
+ "tool_details": {
2844
+ tool_name: self.get_agent_info(tool_name)
2845
+ for tool_name in self.agents.keys()
2846
+ },
2847
+ }
2848
+
2849
+ # Add queue information if enabled
2850
+ if self.queue_enabled:
2851
+ info["queue_config"] = {
2852
+ "max_workers_per_agent": self.max_workers_per_agent,
2853
+ "max_queue_size_per_agent": self.max_queue_size_per_agent,
2854
+ "processing_timeout": self.processing_timeout,
2855
+ "retry_delay": self.retry_delay,
2856
+ }
2857
+
2858
+ # Add queue stats for each agent
2859
+ queue_stats = {}
2860
+ for tool_name in self.agents.keys():
2861
+ if tool_name in self.task_queues:
2862
+ stats = self.task_queues[tool_name].get_stats()
2863
+ queue_stats[tool_name] = {
2864
+ "status": self.task_queues[tool_name]
2865
+ .get_status()
2866
+ .value,
2867
+ "total_tasks": stats.total_tasks,
2868
+ "completed_tasks": stats.completed_tasks,
2869
+ "failed_tasks": stats.failed_tasks,
2870
+ "pending_tasks": stats.pending_tasks,
2871
+ "processing_tasks": stats.processing_tasks,
2872
+ "average_processing_time": stats.average_processing_time,
2873
+ "queue_size": stats.queue_size,
2874
+ }
2875
+
2876
+ info["queue_stats"] = queue_stats
2877
+
2878
+ if self.verbose:
2879
+ logger.debug(f"Retrieved server info: {info}")
2880
+
2881
+ return info
2882
+
2883
+
2884
+ class AOPCluster:
2885
+ """
2886
+ AOPCluster manages a cluster of MCP servers, allowing for the retrieval and searching
2887
+ of tools (agents) across multiple endpoints.
2888
+
2889
+ Attributes:
2890
+ urls (List[str]): List of MCP server URLs to connect to.
2891
+ transport (str): The transport protocol to use (default: "streamable-http").
2892
+ """
2893
+
2894
+ def __init__(
2895
+ self,
2896
+ urls: List[str],
2897
+ transport: str = "streamable-http",
2898
+ *args,
2899
+ **kwargs,
2900
+ ):
2901
+ """
2902
+ Initialize the AOPCluster.
2903
+
2904
+ Args:
2905
+ urls (List[str]): List of MCP server URLs.
2906
+ transport (str, optional): Transport protocol to use. Defaults to "streamable-http".
2907
+ *args: Additional positional arguments.
2908
+ **kwargs: Additional keyword arguments.
2909
+ """
2910
+ self.urls = urls
2911
+ self.transport = transport
2912
+
2913
+ def get_tools(
2914
+ self, output_type: Literal["json", "dict", "str"] = "dict"
2915
+ ) -> List[Dict[str, Any]]:
2916
+ """
2917
+ Retrieve the list of tools (agents) from all MCP servers in the cluster.
2918
+
2919
+ Args:
2920
+ output_type (Literal["json", "dict", "str"], optional): The format of the output.
2921
+ Can be "json", "dict", or "str". Defaults to "dict".
2922
+
2923
+ Returns:
2924
+ List[Dict[str, Any]]: A list of tool information dictionaries.
2925
+ """
2926
+ return get_tools_for_multiple_mcp_servers(
2927
+ urls=self.urls,
2928
+ format="openai",
2929
+ output_type=output_type,
2930
+ transport=self.transport,
2931
+ )
2932
+
2933
+ def find_tool_by_server_name(
2934
+ self, server_name: str
2935
+ ) -> Dict[str, Any]:
2936
+ """
2937
+ Find a tool by its server name (function name).
2938
+
2939
+ Args:
2940
+ server_name (str): The name of the tool/function to find.
2941
+
2942
+ Returns:
2943
+ Dict[str, Any]: Dictionary containing the tool information, or None if not found.
2944
+ """
2945
+ for tool in self.get_tools(output_type="dict"):
2946
+ if tool.get("function", {}).get("name") == server_name:
2947
+ return tool
2948
+ return None