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.
- .github/ISSUE_TEMPLATE/bug_report.md +65 -0
- .github/ISSUE_TEMPLATE/feature_request.md +41 -0
- .github/PULL_REQUEST_TEMPLATE.md +20 -0
- .github/workflows/publish-manual.yml +61 -0
- .github/workflows/publish.yml +64 -0
- .gitignore +214 -0
- CRCA.py +4156 -0
- LICENSE +201 -0
- MANIFEST.in +43 -0
- PKG-INFO +5035 -0
- README.md +4959 -0
- __init__.py +17 -0
- branches/CRCA-Q.py +2728 -0
- branches/crca_cg/corposwarm.py +9065 -0
- branches/crca_cg/fix_rancher_docker_creds.ps1 +155 -0
- branches/crca_cg/package.json +5 -0
- branches/crca_cg/test_bolt_integration.py +446 -0
- branches/crca_cg/test_corposwarm_comprehensive.py +773 -0
- branches/crca_cg/test_new_features.py +163 -0
- branches/crca_sd/__init__.py +149 -0
- branches/crca_sd/crca_sd_core.py +770 -0
- branches/crca_sd/crca_sd_governance.py +1325 -0
- branches/crca_sd/crca_sd_mpc.py +1130 -0
- branches/crca_sd/crca_sd_realtime.py +1844 -0
- branches/crca_sd/crca_sd_tui.py +1133 -0
- crca-1.4.0.dist-info/METADATA +5035 -0
- crca-1.4.0.dist-info/RECORD +501 -0
- crca-1.4.0.dist-info/WHEEL +4 -0
- crca-1.4.0.dist-info/licenses/LICENSE +201 -0
- docs/CRCA-Q.md +2333 -0
- examples/config.yaml.example +25 -0
- examples/crca_sd_example.py +513 -0
- examples/data_broker_example.py +294 -0
- examples/logistics_corporation.py +861 -0
- examples/palantir_example.py +299 -0
- examples/policy_bench.py +934 -0
- examples/pridnestrovia-sd.py +705 -0
- examples/pridnestrovia_realtime.py +1902 -0
- prompts/__init__.py +10 -0
- prompts/default_crca.py +101 -0
- pyproject.toml +151 -0
- requirements.txt +76 -0
- schemas/__init__.py +43 -0
- schemas/mcpSchemas.py +51 -0
- schemas/policy.py +458 -0
- templates/__init__.py +38 -0
- templates/base_specialized_agent.py +195 -0
- templates/drift_detection.py +325 -0
- templates/examples/causal_agent_template.py +309 -0
- templates/examples/drag_drop_example.py +213 -0
- templates/examples/logistics_agent_template.py +207 -0
- templates/examples/trading_agent_template.py +206 -0
- templates/feature_mixins.py +253 -0
- templates/graph_management.py +442 -0
- templates/llm_integration.py +194 -0
- templates/module_registry.py +276 -0
- templates/mpc_planner.py +280 -0
- templates/policy_loop.py +1168 -0
- templates/prediction_framework.py +448 -0
- templates/statistical_methods.py +778 -0
- tests/sanity.yml +31 -0
- tests/sanity_check +406 -0
- tests/test_core.py +47 -0
- tests/test_crca_excel.py +166 -0
- tests/test_crca_sd.py +780 -0
- tests/test_data_broker.py +424 -0
- tests/test_palantir.py +349 -0
- tools/__init__.py +38 -0
- tools/actuators.py +437 -0
- tools/bolt.diy/Dockerfile +103 -0
- tools/bolt.diy/app/components/@settings/core/AvatarDropdown.tsx +175 -0
- tools/bolt.diy/app/components/@settings/core/ControlPanel.tsx +345 -0
- tools/bolt.diy/app/components/@settings/core/constants.tsx +108 -0
- tools/bolt.diy/app/components/@settings/core/types.ts +114 -0
- tools/bolt.diy/app/components/@settings/index.ts +12 -0
- tools/bolt.diy/app/components/@settings/shared/components/TabTile.tsx +151 -0
- tools/bolt.diy/app/components/@settings/shared/service-integration/ConnectionForm.tsx +193 -0
- tools/bolt.diy/app/components/@settings/shared/service-integration/ConnectionTestIndicator.tsx +60 -0
- tools/bolt.diy/app/components/@settings/shared/service-integration/ErrorState.tsx +102 -0
- tools/bolt.diy/app/components/@settings/shared/service-integration/LoadingState.tsx +94 -0
- tools/bolt.diy/app/components/@settings/shared/service-integration/ServiceHeader.tsx +72 -0
- tools/bolt.diy/app/components/@settings/shared/service-integration/index.ts +6 -0
- tools/bolt.diy/app/components/@settings/tabs/data/DataTab.tsx +721 -0
- tools/bolt.diy/app/components/@settings/tabs/data/DataVisualization.tsx +384 -0
- tools/bolt.diy/app/components/@settings/tabs/event-logs/EventLogsTab.tsx +1013 -0
- tools/bolt.diy/app/components/@settings/tabs/features/FeaturesTab.tsx +295 -0
- tools/bolt.diy/app/components/@settings/tabs/github/GitHubTab.tsx +281 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubAuthDialog.tsx +173 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubCacheManager.tsx +367 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubConnection.tsx +233 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubErrorBoundary.tsx +105 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubProgressiveLoader.tsx +266 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubRepositoryCard.tsx +121 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubRepositorySelector.tsx +312 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubStats.tsx +291 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/GitHubUserProfile.tsx +46 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/shared/GitHubStateIndicators.tsx +264 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/shared/RepositoryCard.tsx +361 -0
- tools/bolt.diy/app/components/@settings/tabs/github/components/shared/index.ts +11 -0
- tools/bolt.diy/app/components/@settings/tabs/gitlab/GitLabTab.tsx +305 -0
- tools/bolt.diy/app/components/@settings/tabs/gitlab/components/GitLabAuthDialog.tsx +186 -0
- tools/bolt.diy/app/components/@settings/tabs/gitlab/components/GitLabConnection.tsx +253 -0
- tools/bolt.diy/app/components/@settings/tabs/gitlab/components/GitLabRepositorySelector.tsx +358 -0
- tools/bolt.diy/app/components/@settings/tabs/gitlab/components/RepositoryCard.tsx +79 -0
- tools/bolt.diy/app/components/@settings/tabs/gitlab/components/RepositoryList.tsx +142 -0
- tools/bolt.diy/app/components/@settings/tabs/gitlab/components/StatsDisplay.tsx +91 -0
- tools/bolt.diy/app/components/@settings/tabs/gitlab/components/index.ts +4 -0
- tools/bolt.diy/app/components/@settings/tabs/mcp/McpServerList.tsx +99 -0
- tools/bolt.diy/app/components/@settings/tabs/mcp/McpServerListItem.tsx +70 -0
- tools/bolt.diy/app/components/@settings/tabs/mcp/McpStatusBadge.tsx +37 -0
- tools/bolt.diy/app/components/@settings/tabs/mcp/McpTab.tsx +239 -0
- tools/bolt.diy/app/components/@settings/tabs/netlify/NetlifyTab.tsx +1393 -0
- tools/bolt.diy/app/components/@settings/tabs/netlify/components/NetlifyConnection.tsx +990 -0
- tools/bolt.diy/app/components/@settings/tabs/netlify/components/index.ts +1 -0
- tools/bolt.diy/app/components/@settings/tabs/notifications/NotificationsTab.tsx +300 -0
- tools/bolt.diy/app/components/@settings/tabs/profile/ProfileTab.tsx +181 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/cloud/CloudProvidersTab.tsx +308 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/local/ErrorBoundary.tsx +68 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/local/HealthStatusBadge.tsx +64 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/local/LoadingSkeleton.tsx +107 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/local/LocalProvidersTab.tsx +556 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/local/ModelCard.tsx +106 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/local/ProviderCard.tsx +120 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/local/SetupGuide.tsx +671 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/local/StatusDashboard.tsx +91 -0
- tools/bolt.diy/app/components/@settings/tabs/providers/local/types.ts +44 -0
- tools/bolt.diy/app/components/@settings/tabs/settings/SettingsTab.tsx +215 -0
- tools/bolt.diy/app/components/@settings/tabs/supabase/SupabaseTab.tsx +1089 -0
- tools/bolt.diy/app/components/@settings/tabs/vercel/VercelTab.tsx +909 -0
- tools/bolt.diy/app/components/@settings/tabs/vercel/components/VercelConnection.tsx +368 -0
- tools/bolt.diy/app/components/@settings/tabs/vercel/components/index.ts +1 -0
- tools/bolt.diy/app/components/@settings/utils/tab-helpers.ts +54 -0
- tools/bolt.diy/app/components/chat/APIKeyManager.tsx +169 -0
- tools/bolt.diy/app/components/chat/Artifact.tsx +296 -0
- tools/bolt.diy/app/components/chat/AssistantMessage.tsx +192 -0
- tools/bolt.diy/app/components/chat/BaseChat.module.scss +47 -0
- tools/bolt.diy/app/components/chat/BaseChat.tsx +522 -0
- tools/bolt.diy/app/components/chat/Chat.client.tsx +670 -0
- tools/bolt.diy/app/components/chat/ChatAlert.tsx +108 -0
- tools/bolt.diy/app/components/chat/ChatBox.tsx +334 -0
- tools/bolt.diy/app/components/chat/CodeBlock.module.scss +10 -0
- tools/bolt.diy/app/components/chat/CodeBlock.tsx +85 -0
- tools/bolt.diy/app/components/chat/DicussMode.tsx +17 -0
- tools/bolt.diy/app/components/chat/ExamplePrompts.tsx +37 -0
- tools/bolt.diy/app/components/chat/FilePreview.tsx +38 -0
- tools/bolt.diy/app/components/chat/GitCloneButton.tsx +327 -0
- tools/bolt.diy/app/components/chat/ImportFolderButton.tsx +141 -0
- tools/bolt.diy/app/components/chat/LLMApiAlert.tsx +109 -0
- tools/bolt.diy/app/components/chat/MCPTools.tsx +129 -0
- tools/bolt.diy/app/components/chat/Markdown.module.scss +171 -0
- tools/bolt.diy/app/components/chat/Markdown.spec.ts +48 -0
- tools/bolt.diy/app/components/chat/Markdown.tsx +252 -0
- tools/bolt.diy/app/components/chat/Messages.client.tsx +102 -0
- tools/bolt.diy/app/components/chat/ModelSelector.tsx +797 -0
- tools/bolt.diy/app/components/chat/NetlifyDeploymentLink.client.tsx +51 -0
- tools/bolt.diy/app/components/chat/ProgressCompilation.tsx +110 -0
- tools/bolt.diy/app/components/chat/ScreenshotStateManager.tsx +33 -0
- tools/bolt.diy/app/components/chat/SendButton.client.tsx +39 -0
- tools/bolt.diy/app/components/chat/SpeechRecognition.tsx +28 -0
- tools/bolt.diy/app/components/chat/StarterTemplates.tsx +38 -0
- tools/bolt.diy/app/components/chat/SupabaseAlert.tsx +199 -0
- tools/bolt.diy/app/components/chat/SupabaseConnection.tsx +339 -0
- tools/bolt.diy/app/components/chat/ThoughtBox.tsx +43 -0
- tools/bolt.diy/app/components/chat/ToolInvocations.tsx +409 -0
- tools/bolt.diy/app/components/chat/UserMessage.tsx +101 -0
- tools/bolt.diy/app/components/chat/VercelDeploymentLink.client.tsx +158 -0
- tools/bolt.diy/app/components/chat/chatExportAndImport/ExportChatButton.tsx +49 -0
- tools/bolt.diy/app/components/chat/chatExportAndImport/ImportButtons.tsx +96 -0
- tools/bolt.diy/app/components/deploy/DeployAlert.tsx +197 -0
- tools/bolt.diy/app/components/deploy/DeployButton.tsx +277 -0
- tools/bolt.diy/app/components/deploy/GitHubDeploy.client.tsx +171 -0
- tools/bolt.diy/app/components/deploy/GitHubDeploymentDialog.tsx +1041 -0
- tools/bolt.diy/app/components/deploy/GitLabDeploy.client.tsx +171 -0
- tools/bolt.diy/app/components/deploy/GitLabDeploymentDialog.tsx +764 -0
- tools/bolt.diy/app/components/deploy/NetlifyDeploy.client.tsx +246 -0
- tools/bolt.diy/app/components/deploy/VercelDeploy.client.tsx +235 -0
- tools/bolt.diy/app/components/editor/codemirror/BinaryContent.tsx +7 -0
- tools/bolt.diy/app/components/editor/codemirror/CodeMirrorEditor.tsx +555 -0
- tools/bolt.diy/app/components/editor/codemirror/EnvMasking.ts +80 -0
- tools/bolt.diy/app/components/editor/codemirror/cm-theme.ts +192 -0
- tools/bolt.diy/app/components/editor/codemirror/indent.ts +68 -0
- tools/bolt.diy/app/components/editor/codemirror/languages.ts +112 -0
- tools/bolt.diy/app/components/git/GitUrlImport.client.tsx +147 -0
- tools/bolt.diy/app/components/header/Header.tsx +42 -0
- tools/bolt.diy/app/components/header/HeaderActionButtons.client.tsx +54 -0
- tools/bolt.diy/app/components/mandate/MandateSubmission.tsx +167 -0
- tools/bolt.diy/app/components/observability/DeploymentStatus.tsx +168 -0
- tools/bolt.diy/app/components/observability/EventTimeline.tsx +119 -0
- tools/bolt.diy/app/components/observability/FileDiffViewer.tsx +121 -0
- tools/bolt.diy/app/components/observability/GovernanceStatus.tsx +197 -0
- tools/bolt.diy/app/components/observability/GovernorMetrics.tsx +246 -0
- tools/bolt.diy/app/components/observability/LogStream.tsx +244 -0
- tools/bolt.diy/app/components/observability/MandateDetails.tsx +201 -0
- tools/bolt.diy/app/components/observability/ObservabilityDashboard.tsx +200 -0
- tools/bolt.diy/app/components/sidebar/HistoryItem.tsx +187 -0
- tools/bolt.diy/app/components/sidebar/Menu.client.tsx +536 -0
- tools/bolt.diy/app/components/sidebar/date-binning.ts +59 -0
- tools/bolt.diy/app/components/txt +1 -0
- tools/bolt.diy/app/components/ui/BackgroundRays/index.tsx +18 -0
- tools/bolt.diy/app/components/ui/BackgroundRays/styles.module.scss +246 -0
- tools/bolt.diy/app/components/ui/Badge.tsx +53 -0
- tools/bolt.diy/app/components/ui/BranchSelector.tsx +270 -0
- tools/bolt.diy/app/components/ui/Breadcrumbs.tsx +101 -0
- tools/bolt.diy/app/components/ui/Button.tsx +46 -0
- tools/bolt.diy/app/components/ui/Card.tsx +55 -0
- tools/bolt.diy/app/components/ui/Checkbox.tsx +32 -0
- tools/bolt.diy/app/components/ui/CloseButton.tsx +49 -0
- tools/bolt.diy/app/components/ui/CodeBlock.tsx +103 -0
- tools/bolt.diy/app/components/ui/Collapsible.tsx +9 -0
- tools/bolt.diy/app/components/ui/ColorSchemeDialog.tsx +378 -0
- tools/bolt.diy/app/components/ui/Dialog.tsx +449 -0
- tools/bolt.diy/app/components/ui/Dropdown.tsx +63 -0
- tools/bolt.diy/app/components/ui/EmptyState.tsx +154 -0
- tools/bolt.diy/app/components/ui/FileIcon.tsx +346 -0
- tools/bolt.diy/app/components/ui/FilterChip.tsx +92 -0
- tools/bolt.diy/app/components/ui/GlowingEffect.tsx +192 -0
- tools/bolt.diy/app/components/ui/GradientCard.tsx +100 -0
- tools/bolt.diy/app/components/ui/IconButton.tsx +84 -0
- tools/bolt.diy/app/components/ui/Input.tsx +22 -0
- tools/bolt.diy/app/components/ui/Label.tsx +20 -0
- tools/bolt.diy/app/components/ui/LoadingDots.tsx +27 -0
- tools/bolt.diy/app/components/ui/LoadingOverlay.tsx +32 -0
- tools/bolt.diy/app/components/ui/PanelHeader.tsx +20 -0
- tools/bolt.diy/app/components/ui/PanelHeaderButton.tsx +36 -0
- tools/bolt.diy/app/components/ui/Popover.tsx +29 -0
- tools/bolt.diy/app/components/ui/Progress.tsx +22 -0
- tools/bolt.diy/app/components/ui/RepositoryStats.tsx +87 -0
- tools/bolt.diy/app/components/ui/ScrollArea.tsx +41 -0
- tools/bolt.diy/app/components/ui/SearchInput.tsx +80 -0
- tools/bolt.diy/app/components/ui/SearchResultItem.tsx +134 -0
- tools/bolt.diy/app/components/ui/Separator.tsx +22 -0
- tools/bolt.diy/app/components/ui/SettingsButton.tsx +35 -0
- tools/bolt.diy/app/components/ui/Slider.tsx +73 -0
- tools/bolt.diy/app/components/ui/StatusIndicator.tsx +90 -0
- tools/bolt.diy/app/components/ui/Switch.tsx +37 -0
- tools/bolt.diy/app/components/ui/Tabs.tsx +52 -0
- tools/bolt.diy/app/components/ui/TabsWithSlider.tsx +112 -0
- tools/bolt.diy/app/components/ui/ThemeSwitch.tsx +29 -0
- tools/bolt.diy/app/components/ui/Tooltip.tsx +122 -0
- tools/bolt.diy/app/components/ui/index.ts +38 -0
- tools/bolt.diy/app/components/ui/use-toast.ts +66 -0
- tools/bolt.diy/app/components/workbench/DiffView.tsx +796 -0
- tools/bolt.diy/app/components/workbench/EditorPanel.tsx +174 -0
- tools/bolt.diy/app/components/workbench/ExpoQrModal.tsx +55 -0
- tools/bolt.diy/app/components/workbench/FileBreadcrumb.tsx +150 -0
- tools/bolt.diy/app/components/workbench/FileTree.tsx +565 -0
- tools/bolt.diy/app/components/workbench/Inspector.tsx +126 -0
- tools/bolt.diy/app/components/workbench/InspectorPanel.tsx +146 -0
- tools/bolt.diy/app/components/workbench/LockManager.tsx +262 -0
- tools/bolt.diy/app/components/workbench/PortDropdown.tsx +91 -0
- tools/bolt.diy/app/components/workbench/Preview.tsx +1049 -0
- tools/bolt.diy/app/components/workbench/ScreenshotSelector.tsx +293 -0
- tools/bolt.diy/app/components/workbench/Search.tsx +257 -0
- tools/bolt.diy/app/components/workbench/Workbench.client.tsx +506 -0
- tools/bolt.diy/app/components/workbench/terminal/Terminal.tsx +131 -0
- tools/bolt.diy/app/components/workbench/terminal/TerminalManager.tsx +68 -0
- tools/bolt.diy/app/components/workbench/terminal/TerminalTabs.tsx +277 -0
- tools/bolt.diy/app/components/workbench/terminal/theme.ts +36 -0
- tools/bolt.diy/app/components/workflow/WorkflowPhase.tsx +109 -0
- tools/bolt.diy/app/components/workflow/WorkflowStatus.tsx +60 -0
- tools/bolt.diy/app/components/workflow/WorkflowTimeline.tsx +150 -0
- tools/bolt.diy/app/entry.client.tsx +7 -0
- tools/bolt.diy/app/entry.server.tsx +80 -0
- tools/bolt.diy/app/root.tsx +156 -0
- tools/bolt.diy/app/routes/_index.tsx +175 -0
- tools/bolt.diy/app/routes/api.bug-report.ts +254 -0
- tools/bolt.diy/app/routes/api.chat.ts +463 -0
- tools/bolt.diy/app/routes/api.check-env-key.ts +41 -0
- tools/bolt.diy/app/routes/api.configured-providers.ts +110 -0
- tools/bolt.diy/app/routes/api.corporate-swarm-status.ts +55 -0
- tools/bolt.diy/app/routes/api.enhancer.ts +137 -0
- tools/bolt.diy/app/routes/api.export-api-keys.ts +44 -0
- tools/bolt.diy/app/routes/api.git-info.ts +69 -0
- tools/bolt.diy/app/routes/api.git-proxy.$.ts +178 -0
- tools/bolt.diy/app/routes/api.github-branches.ts +166 -0
- tools/bolt.diy/app/routes/api.github-deploy.ts +67 -0
- tools/bolt.diy/app/routes/api.github-stats.ts +198 -0
- tools/bolt.diy/app/routes/api.github-template.ts +242 -0
- tools/bolt.diy/app/routes/api.github-user.ts +287 -0
- tools/bolt.diy/app/routes/api.gitlab-branches.ts +143 -0
- tools/bolt.diy/app/routes/api.gitlab-deploy.ts +67 -0
- tools/bolt.diy/app/routes/api.gitlab-projects.ts +105 -0
- tools/bolt.diy/app/routes/api.health.ts +8 -0
- tools/bolt.diy/app/routes/api.llmcall.ts +298 -0
- tools/bolt.diy/app/routes/api.mandate.ts +351 -0
- tools/bolt.diy/app/routes/api.mcp-check.ts +16 -0
- tools/bolt.diy/app/routes/api.mcp-update-config.ts +23 -0
- tools/bolt.diy/app/routes/api.models.$provider.ts +2 -0
- tools/bolt.diy/app/routes/api.models.ts +90 -0
- tools/bolt.diy/app/routes/api.netlify-deploy.ts +240 -0
- tools/bolt.diy/app/routes/api.netlify-user.ts +142 -0
- tools/bolt.diy/app/routes/api.supabase-user.ts +199 -0
- tools/bolt.diy/app/routes/api.supabase.query.ts +92 -0
- tools/bolt.diy/app/routes/api.supabase.ts +56 -0
- tools/bolt.diy/app/routes/api.supabase.variables.ts +32 -0
- tools/bolt.diy/app/routes/api.system.diagnostics.ts +142 -0
- tools/bolt.diy/app/routes/api.system.disk-info.ts +311 -0
- tools/bolt.diy/app/routes/api.system.git-info.ts +332 -0
- tools/bolt.diy/app/routes/api.update.ts +21 -0
- tools/bolt.diy/app/routes/api.vercel-deploy.ts +497 -0
- tools/bolt.diy/app/routes/api.vercel-user.ts +161 -0
- tools/bolt.diy/app/routes/api.workflow-status.$proposalId.ts +309 -0
- tools/bolt.diy/app/routes/chat.$id.tsx +8 -0
- tools/bolt.diy/app/routes/execute.$mandateId.tsx +432 -0
- tools/bolt.diy/app/routes/git.tsx +25 -0
- tools/bolt.diy/app/routes/observability.$mandateId.tsx +50 -0
- tools/bolt.diy/app/routes/webcontainer.connect.$id.tsx +32 -0
- tools/bolt.diy/app/routes/webcontainer.preview.$id.tsx +97 -0
- tools/bolt.diy/app/routes/workflow.$proposalId.tsx +170 -0
- tools/bolt.diy/app/styles/animations.scss +49 -0
- tools/bolt.diy/app/styles/components/code.scss +9 -0
- tools/bolt.diy/app/styles/components/editor.scss +135 -0
- tools/bolt.diy/app/styles/components/resize-handle.scss +30 -0
- tools/bolt.diy/app/styles/components/terminal.scss +3 -0
- tools/bolt.diy/app/styles/components/toast.scss +23 -0
- tools/bolt.diy/app/styles/diff-view.css +72 -0
- tools/bolt.diy/app/styles/index.scss +73 -0
- tools/bolt.diy/app/styles/variables.scss +255 -0
- tools/bolt.diy/app/styles/z-index.scss +37 -0
- tools/bolt.diy/app/types/GitHub.ts +182 -0
- tools/bolt.diy/app/types/GitLab.ts +103 -0
- tools/bolt.diy/app/types/actions.ts +85 -0
- tools/bolt.diy/app/types/artifact.ts +5 -0
- tools/bolt.diy/app/types/context.ts +26 -0
- tools/bolt.diy/app/types/design-scheme.ts +93 -0
- tools/bolt.diy/app/types/global.d.ts +13 -0
- tools/bolt.diy/app/types/mandate.ts +333 -0
- tools/bolt.diy/app/types/model.ts +25 -0
- tools/bolt.diy/app/types/netlify.ts +94 -0
- tools/bolt.diy/app/types/supabase.ts +54 -0
- tools/bolt.diy/app/types/template.ts +8 -0
- tools/bolt.diy/app/types/terminal.ts +9 -0
- tools/bolt.diy/app/types/theme.ts +1 -0
- tools/bolt.diy/app/types/vercel.ts +67 -0
- tools/bolt.diy/app/utils/buffer.ts +29 -0
- tools/bolt.diy/app/utils/classNames.ts +65 -0
- tools/bolt.diy/app/utils/constants.ts +147 -0
- tools/bolt.diy/app/utils/debounce.ts +13 -0
- tools/bolt.diy/app/utils/debugLogger.ts +1284 -0
- tools/bolt.diy/app/utils/diff.spec.ts +11 -0
- tools/bolt.diy/app/utils/diff.ts +117 -0
- tools/bolt.diy/app/utils/easings.ts +3 -0
- tools/bolt.diy/app/utils/fileLocks.ts +96 -0
- tools/bolt.diy/app/utils/fileUtils.ts +121 -0
- tools/bolt.diy/app/utils/folderImport.ts +73 -0
- tools/bolt.diy/app/utils/formatSize.ts +12 -0
- tools/bolt.diy/app/utils/getLanguageFromExtension.ts +24 -0
- tools/bolt.diy/app/utils/githubStats.ts +9 -0
- tools/bolt.diy/app/utils/gitlabStats.ts +54 -0
- tools/bolt.diy/app/utils/logger.ts +162 -0
- tools/bolt.diy/app/utils/markdown.ts +155 -0
- tools/bolt.diy/app/utils/mobile.ts +4 -0
- tools/bolt.diy/app/utils/os.ts +4 -0
- tools/bolt.diy/app/utils/path.ts +19 -0
- tools/bolt.diy/app/utils/projectCommands.ts +197 -0
- tools/bolt.diy/app/utils/promises.ts +19 -0
- tools/bolt.diy/app/utils/react.ts +6 -0
- tools/bolt.diy/app/utils/sampler.ts +49 -0
- tools/bolt.diy/app/utils/selectStarterTemplate.ts +255 -0
- tools/bolt.diy/app/utils/shell.ts +384 -0
- tools/bolt.diy/app/utils/stacktrace.ts +27 -0
- tools/bolt.diy/app/utils/stripIndent.ts +23 -0
- tools/bolt.diy/app/utils/terminal.ts +11 -0
- tools/bolt.diy/app/utils/unreachable.ts +3 -0
- tools/bolt.diy/app/vite-env.d.ts +2 -0
- tools/bolt.diy/assets/entitlements.mac.plist +25 -0
- tools/bolt.diy/assets/icons/icon.icns +0 -0
- tools/bolt.diy/assets/icons/icon.ico +0 -0
- tools/bolt.diy/assets/icons/icon.png +0 -0
- tools/bolt.diy/bindings.js +78 -0
- tools/bolt.diy/bindings.sh +33 -0
- tools/bolt.diy/docker-compose.yaml +145 -0
- tools/bolt.diy/electron/main/index.ts +201 -0
- tools/bolt.diy/electron/main/tsconfig.json +30 -0
- tools/bolt.diy/electron/main/ui/menu.ts +29 -0
- tools/bolt.diy/electron/main/ui/window.ts +54 -0
- tools/bolt.diy/electron/main/utils/auto-update.ts +110 -0
- tools/bolt.diy/electron/main/utils/constants.ts +4 -0
- tools/bolt.diy/electron/main/utils/cookie.ts +40 -0
- tools/bolt.diy/electron/main/utils/reload.ts +35 -0
- tools/bolt.diy/electron/main/utils/serve.ts +71 -0
- tools/bolt.diy/electron/main/utils/store.ts +3 -0
- tools/bolt.diy/electron/main/utils/vite-server.ts +44 -0
- tools/bolt.diy/electron/main/vite.config.ts +44 -0
- tools/bolt.diy/electron/preload/index.ts +22 -0
- tools/bolt.diy/electron/preload/tsconfig.json +7 -0
- tools/bolt.diy/electron/preload/vite.config.ts +31 -0
- tools/bolt.diy/electron-builder.yml +64 -0
- tools/bolt.diy/electron-update.yml +4 -0
- tools/bolt.diy/eslint.config.mjs +57 -0
- tools/bolt.diy/functions/[[path]].ts +12 -0
- tools/bolt.diy/icons/angular.svg +1 -0
- tools/bolt.diy/icons/astro.svg +8 -0
- tools/bolt.diy/icons/chat.svg +1 -0
- tools/bolt.diy/icons/expo-brand.svg +1 -0
- tools/bolt.diy/icons/expo.svg +4 -0
- tools/bolt.diy/icons/logo-text.svg +1 -0
- tools/bolt.diy/icons/logo.svg +4 -0
- tools/bolt.diy/icons/mcp.svg +1 -0
- tools/bolt.diy/icons/nativescript.svg +1 -0
- tools/bolt.diy/icons/netlify.svg +10 -0
- tools/bolt.diy/icons/nextjs.svg +1 -0
- tools/bolt.diy/icons/nuxt.svg +1 -0
- tools/bolt.diy/icons/qwik.svg +1 -0
- tools/bolt.diy/icons/react.svg +1 -0
- tools/bolt.diy/icons/remix.svg +24 -0
- tools/bolt.diy/icons/remotion.svg +1 -0
- tools/bolt.diy/icons/shadcn.svg +21 -0
- tools/bolt.diy/icons/slidev.svg +60 -0
- tools/bolt.diy/icons/solidjs.svg +1 -0
- tools/bolt.diy/icons/stars.svg +1 -0
- tools/bolt.diy/icons/svelte.svg +1 -0
- tools/bolt.diy/icons/typescript.svg +1 -0
- tools/bolt.diy/icons/vite.svg +1 -0
- tools/bolt.diy/icons/vue.svg +1 -0
- tools/bolt.diy/load-context.ts +9 -0
- tools/bolt.diy/notarize.cjs +31 -0
- tools/bolt.diy/package.json +218 -0
- tools/bolt.diy/playwright.config.preview.ts +35 -0
- tools/bolt.diy/pre-start.cjs +26 -0
- tools/bolt.diy/public/apple-touch-icon-precomposed.png +0 -0
- tools/bolt.diy/public/apple-touch-icon.png +0 -0
- tools/bolt.diy/public/favicon.ico +0 -0
- tools/bolt.diy/public/favicon.svg +4 -0
- tools/bolt.diy/public/icons/AmazonBedrock.svg +1 -0
- tools/bolt.diy/public/icons/Anthropic.svg +4 -0
- tools/bolt.diy/public/icons/Cohere.svg +4 -0
- tools/bolt.diy/public/icons/Deepseek.svg +5 -0
- tools/bolt.diy/public/icons/Default.svg +4 -0
- tools/bolt.diy/public/icons/Google.svg +4 -0
- tools/bolt.diy/public/icons/Groq.svg +4 -0
- tools/bolt.diy/public/icons/HuggingFace.svg +4 -0
- tools/bolt.diy/public/icons/Hyperbolic.svg +3 -0
- tools/bolt.diy/public/icons/LMStudio.svg +5 -0
- tools/bolt.diy/public/icons/Mistral.svg +4 -0
- tools/bolt.diy/public/icons/Ollama.svg +4 -0
- tools/bolt.diy/public/icons/OpenAI.svg +4 -0
- tools/bolt.diy/public/icons/OpenAILike.svg +4 -0
- tools/bolt.diy/public/icons/OpenRouter.svg +4 -0
- tools/bolt.diy/public/icons/Perplexity.svg +4 -0
- tools/bolt.diy/public/icons/Together.svg +4 -0
- tools/bolt.diy/public/icons/xAI.svg +5 -0
- tools/bolt.diy/public/inspector-script.js +292 -0
- tools/bolt.diy/public/logo-dark-styled.png +0 -0
- tools/bolt.diy/public/logo-dark.png +0 -0
- tools/bolt.diy/public/logo-light-styled.png +0 -0
- tools/bolt.diy/public/logo-light.png +0 -0
- tools/bolt.diy/public/logo.svg +15 -0
- tools/bolt.diy/public/social_preview_index.jpg +0 -0
- tools/bolt.diy/scripts/clean.js +45 -0
- tools/bolt.diy/scripts/electron-dev.mjs +181 -0
- tools/bolt.diy/scripts/setup-env.sh +41 -0
- tools/bolt.diy/scripts/update-imports.sh +7 -0
- tools/bolt.diy/scripts/update.sh +52 -0
- tools/bolt.diy/services/execution-governor/Dockerfile +41 -0
- tools/bolt.diy/services/execution-governor/config.ts +42 -0
- tools/bolt.diy/services/execution-governor/index.ts +683 -0
- tools/bolt.diy/services/execution-governor/metrics.ts +141 -0
- tools/bolt.diy/services/execution-governor/package.json +31 -0
- tools/bolt.diy/services/execution-governor/priority-queue.ts +139 -0
- tools/bolt.diy/services/execution-governor/tsconfig.json +21 -0
- tools/bolt.diy/services/execution-governor/types.ts +145 -0
- tools/bolt.diy/services/headless-executor/Dockerfile +43 -0
- tools/bolt.diy/services/headless-executor/executor.ts +210 -0
- tools/bolt.diy/services/headless-executor/index.ts +323 -0
- tools/bolt.diy/services/headless-executor/package.json +27 -0
- tools/bolt.diy/services/headless-executor/tsconfig.json +21 -0
- tools/bolt.diy/services/headless-executor/types.ts +38 -0
- tools/bolt.diy/test-workflows.sh +240 -0
- tools/bolt.diy/tests/integration/corporate-swarm.test.ts +208 -0
- tools/bolt.diy/tests/mandates/budget-limited.json +34 -0
- tools/bolt.diy/tests/mandates/complex.json +53 -0
- tools/bolt.diy/tests/mandates/constraint-enforced.json +36 -0
- tools/bolt.diy/tests/mandates/simple.json +35 -0
- tools/bolt.diy/tsconfig.json +37 -0
- tools/bolt.diy/types/istextorbinary.d.ts +15 -0
- tools/bolt.diy/uno.config.ts +279 -0
- tools/bolt.diy/vite-electron.config.ts +76 -0
- tools/bolt.diy/vite.config.ts +112 -0
- tools/bolt.diy/worker-configuration.d.ts +22 -0
- tools/bolt.diy/wrangler.toml +6 -0
- tools/code_generator.py +461 -0
- tools/file_operations.py +465 -0
- tools/mandate_generator.py +337 -0
- tools/mcpClientUtils.py +1216 -0
- tools/sensors.py +285 -0
- utils/Agent_types.py +15 -0
- utils/AnyToStr.py +0 -0
- utils/HHCS.py +277 -0
- utils/__init__.py +30 -0
- utils/agent.py +3627 -0
- utils/aop.py +2948 -0
- utils/canonical.py +143 -0
- utils/conversation.py +1195 -0
- utils/doctrine_versioning +230 -0
- utils/formatter.py +474 -0
- utils/ledger.py +311 -0
- utils/out_types.py +16 -0
- utils/rollback.py +339 -0
- utils/router.py +929 -0
- 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
|