alita-sdk 0.3.257__py3-none-any.whl → 0.3.584__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.
Potentially problematic release.
This version of alita-sdk might be problematic. Click here for more details.
- alita_sdk/cli/__init__.py +10 -0
- alita_sdk/cli/__main__.py +17 -0
- alita_sdk/cli/agent/__init__.py +5 -0
- alita_sdk/cli/agent/default.py +258 -0
- alita_sdk/cli/agent_executor.py +155 -0
- alita_sdk/cli/agent_loader.py +215 -0
- alita_sdk/cli/agent_ui.py +228 -0
- alita_sdk/cli/agents.py +3794 -0
- alita_sdk/cli/callbacks.py +647 -0
- alita_sdk/cli/cli.py +168 -0
- alita_sdk/cli/config.py +306 -0
- alita_sdk/cli/context/__init__.py +30 -0
- alita_sdk/cli/context/cleanup.py +198 -0
- alita_sdk/cli/context/manager.py +731 -0
- alita_sdk/cli/context/message.py +285 -0
- alita_sdk/cli/context/strategies.py +289 -0
- alita_sdk/cli/context/token_estimation.py +127 -0
- alita_sdk/cli/formatting.py +182 -0
- alita_sdk/cli/input_handler.py +419 -0
- alita_sdk/cli/inventory.py +1073 -0
- alita_sdk/cli/mcp_loader.py +315 -0
- alita_sdk/cli/toolkit.py +327 -0
- alita_sdk/cli/toolkit_loader.py +85 -0
- alita_sdk/cli/tools/__init__.py +43 -0
- alita_sdk/cli/tools/approval.py +224 -0
- alita_sdk/cli/tools/filesystem.py +1751 -0
- alita_sdk/cli/tools/planning.py +389 -0
- alita_sdk/cli/tools/terminal.py +414 -0
- alita_sdk/community/__init__.py +72 -12
- alita_sdk/community/inventory/__init__.py +236 -0
- alita_sdk/community/inventory/config.py +257 -0
- alita_sdk/community/inventory/enrichment.py +2137 -0
- alita_sdk/community/inventory/extractors.py +1469 -0
- alita_sdk/community/inventory/ingestion.py +3172 -0
- alita_sdk/community/inventory/knowledge_graph.py +1457 -0
- alita_sdk/community/inventory/parsers/__init__.py +218 -0
- alita_sdk/community/inventory/parsers/base.py +295 -0
- alita_sdk/community/inventory/parsers/csharp_parser.py +907 -0
- alita_sdk/community/inventory/parsers/go_parser.py +851 -0
- alita_sdk/community/inventory/parsers/html_parser.py +389 -0
- alita_sdk/community/inventory/parsers/java_parser.py +593 -0
- alita_sdk/community/inventory/parsers/javascript_parser.py +629 -0
- alita_sdk/community/inventory/parsers/kotlin_parser.py +768 -0
- alita_sdk/community/inventory/parsers/markdown_parser.py +362 -0
- alita_sdk/community/inventory/parsers/python_parser.py +604 -0
- alita_sdk/community/inventory/parsers/rust_parser.py +858 -0
- alita_sdk/community/inventory/parsers/swift_parser.py +832 -0
- alita_sdk/community/inventory/parsers/text_parser.py +322 -0
- alita_sdk/community/inventory/parsers/yaml_parser.py +370 -0
- alita_sdk/community/inventory/patterns/__init__.py +61 -0
- alita_sdk/community/inventory/patterns/ast_adapter.py +380 -0
- alita_sdk/community/inventory/patterns/loader.py +348 -0
- alita_sdk/community/inventory/patterns/registry.py +198 -0
- alita_sdk/community/inventory/presets.py +535 -0
- alita_sdk/community/inventory/retrieval.py +1403 -0
- alita_sdk/community/inventory/toolkit.py +173 -0
- alita_sdk/community/inventory/toolkit_utils.py +176 -0
- alita_sdk/community/inventory/visualize.py +1370 -0
- alita_sdk/configurations/__init__.py +11 -0
- alita_sdk/configurations/ado.py +148 -2
- alita_sdk/configurations/azure_search.py +1 -1
- alita_sdk/configurations/bigquery.py +1 -1
- alita_sdk/configurations/bitbucket.py +94 -2
- alita_sdk/configurations/browser.py +18 -0
- alita_sdk/configurations/carrier.py +19 -0
- alita_sdk/configurations/confluence.py +130 -1
- alita_sdk/configurations/delta_lake.py +1 -1
- alita_sdk/configurations/figma.py +76 -5
- alita_sdk/configurations/github.py +65 -1
- alita_sdk/configurations/gitlab.py +81 -0
- alita_sdk/configurations/google_places.py +17 -0
- alita_sdk/configurations/jira.py +103 -0
- alita_sdk/configurations/openapi.py +323 -0
- alita_sdk/configurations/postman.py +1 -1
- alita_sdk/configurations/qtest.py +72 -3
- alita_sdk/configurations/report_portal.py +115 -0
- alita_sdk/configurations/salesforce.py +19 -0
- alita_sdk/configurations/service_now.py +1 -12
- alita_sdk/configurations/sharepoint.py +167 -0
- alita_sdk/configurations/sonar.py +18 -0
- alita_sdk/configurations/sql.py +20 -0
- alita_sdk/configurations/testio.py +101 -0
- alita_sdk/configurations/testrail.py +88 -0
- alita_sdk/configurations/xray.py +94 -1
- alita_sdk/configurations/zephyr_enterprise.py +94 -1
- alita_sdk/configurations/zephyr_essential.py +95 -0
- alita_sdk/runtime/clients/artifact.py +21 -4
- alita_sdk/runtime/clients/client.py +458 -67
- alita_sdk/runtime/clients/mcp_discovery.py +342 -0
- alita_sdk/runtime/clients/mcp_manager.py +262 -0
- alita_sdk/runtime/clients/sandbox_client.py +352 -0
- alita_sdk/runtime/langchain/_constants_bkup.py +1318 -0
- alita_sdk/runtime/langchain/assistant.py +183 -43
- alita_sdk/runtime/langchain/constants.py +647 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +209 -31
- alita_sdk/runtime/langchain/document_loaders/AlitaImageLoader.py +1 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +10 -3
- alita_sdk/runtime/langchain/document_loaders/AlitaMarkdownLoader.py +66 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaPDFLoader.py +79 -10
- alita_sdk/runtime/langchain/document_loaders/AlitaPowerPointLoader.py +52 -15
- alita_sdk/runtime/langchain/document_loaders/AlitaPythonLoader.py +9 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaTableLoader.py +1 -4
- alita_sdk/runtime/langchain/document_loaders/AlitaTextLoader.py +15 -2
- alita_sdk/runtime/langchain/document_loaders/ImageParser.py +30 -0
- alita_sdk/runtime/langchain/document_loaders/constants.py +189 -41
- alita_sdk/runtime/langchain/interfaces/llm_processor.py +4 -2
- alita_sdk/runtime/langchain/langraph_agent.py +493 -105
- alita_sdk/runtime/langchain/utils.py +118 -8
- alita_sdk/runtime/llms/preloaded.py +2 -6
- alita_sdk/runtime/models/mcp_models.py +61 -0
- alita_sdk/runtime/skills/__init__.py +91 -0
- alita_sdk/runtime/skills/callbacks.py +498 -0
- alita_sdk/runtime/skills/discovery.py +540 -0
- alita_sdk/runtime/skills/executor.py +610 -0
- alita_sdk/runtime/skills/input_builder.py +371 -0
- alita_sdk/runtime/skills/models.py +330 -0
- alita_sdk/runtime/skills/registry.py +355 -0
- alita_sdk/runtime/skills/skill_runner.py +330 -0
- alita_sdk/runtime/toolkits/__init__.py +28 -0
- alita_sdk/runtime/toolkits/application.py +14 -4
- alita_sdk/runtime/toolkits/artifact.py +25 -9
- alita_sdk/runtime/toolkits/datasource.py +13 -6
- alita_sdk/runtime/toolkits/mcp.py +782 -0
- alita_sdk/runtime/toolkits/planning.py +178 -0
- alita_sdk/runtime/toolkits/skill_router.py +238 -0
- alita_sdk/runtime/toolkits/subgraph.py +11 -6
- alita_sdk/runtime/toolkits/tools.py +314 -70
- alita_sdk/runtime/toolkits/vectorstore.py +11 -5
- alita_sdk/runtime/tools/__init__.py +24 -0
- alita_sdk/runtime/tools/application.py +16 -4
- alita_sdk/runtime/tools/artifact.py +367 -33
- alita_sdk/runtime/tools/data_analysis.py +183 -0
- alita_sdk/runtime/tools/function.py +100 -4
- alita_sdk/runtime/tools/graph.py +81 -0
- alita_sdk/runtime/tools/image_generation.py +218 -0
- alita_sdk/runtime/tools/llm.py +1032 -177
- alita_sdk/runtime/tools/loop.py +3 -1
- alita_sdk/runtime/tools/loop_output.py +3 -1
- alita_sdk/runtime/tools/mcp_inspect_tool.py +284 -0
- alita_sdk/runtime/tools/mcp_remote_tool.py +181 -0
- alita_sdk/runtime/tools/mcp_server_tool.py +3 -1
- alita_sdk/runtime/tools/planning/__init__.py +36 -0
- alita_sdk/runtime/tools/planning/models.py +246 -0
- alita_sdk/runtime/tools/planning/wrapper.py +607 -0
- alita_sdk/runtime/tools/router.py +2 -1
- alita_sdk/runtime/tools/sandbox.py +375 -0
- alita_sdk/runtime/tools/skill_router.py +776 -0
- alita_sdk/runtime/tools/tool.py +3 -1
- alita_sdk/runtime/tools/vectorstore.py +69 -65
- alita_sdk/runtime/tools/vectorstore_base.py +163 -90
- alita_sdk/runtime/utils/AlitaCallback.py +137 -21
- alita_sdk/runtime/utils/constants.py +5 -1
- alita_sdk/runtime/utils/mcp_client.py +492 -0
- alita_sdk/runtime/utils/mcp_oauth.py +361 -0
- alita_sdk/runtime/utils/mcp_sse_client.py +434 -0
- alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
- alita_sdk/runtime/utils/streamlit.py +41 -14
- alita_sdk/runtime/utils/toolkit_utils.py +28 -9
- alita_sdk/runtime/utils/utils.py +48 -0
- alita_sdk/tools/__init__.py +135 -37
- alita_sdk/tools/ado/__init__.py +2 -2
- alita_sdk/tools/ado/repos/__init__.py +16 -19
- alita_sdk/tools/ado/repos/repos_wrapper.py +12 -20
- alita_sdk/tools/ado/test_plan/__init__.py +27 -8
- alita_sdk/tools/ado/test_plan/test_plan_wrapper.py +56 -28
- alita_sdk/tools/ado/wiki/__init__.py +28 -12
- alita_sdk/tools/ado/wiki/ado_wrapper.py +114 -40
- alita_sdk/tools/ado/work_item/__init__.py +28 -12
- alita_sdk/tools/ado/work_item/ado_wrapper.py +95 -11
- alita_sdk/tools/advanced_jira_mining/__init__.py +13 -8
- alita_sdk/tools/aws/delta_lake/__init__.py +15 -11
- alita_sdk/tools/aws/delta_lake/tool.py +5 -1
- alita_sdk/tools/azure_ai/search/__init__.py +14 -8
- alita_sdk/tools/base/tool.py +5 -1
- alita_sdk/tools/base_indexer_toolkit.py +454 -110
- alita_sdk/tools/bitbucket/__init__.py +28 -19
- alita_sdk/tools/bitbucket/api_wrapper.py +285 -27
- alita_sdk/tools/bitbucket/cloud_api_wrapper.py +5 -5
- alita_sdk/tools/browser/__init__.py +41 -16
- alita_sdk/tools/browser/crawler.py +3 -1
- alita_sdk/tools/browser/utils.py +15 -6
- alita_sdk/tools/carrier/__init__.py +18 -17
- alita_sdk/tools/carrier/backend_reports_tool.py +8 -4
- alita_sdk/tools/carrier/excel_reporter.py +8 -4
- alita_sdk/tools/chunkers/__init__.py +3 -1
- alita_sdk/tools/chunkers/code/codeparser.py +1 -1
- alita_sdk/tools/chunkers/sematic/json_chunker.py +2 -1
- alita_sdk/tools/chunkers/sematic/markdown_chunker.py +97 -6
- alita_sdk/tools/chunkers/sematic/proposal_chunker.py +1 -1
- alita_sdk/tools/chunkers/universal_chunker.py +270 -0
- alita_sdk/tools/cloud/aws/__init__.py +12 -7
- alita_sdk/tools/cloud/azure/__init__.py +12 -7
- alita_sdk/tools/cloud/gcp/__init__.py +12 -7
- alita_sdk/tools/cloud/k8s/__init__.py +12 -7
- alita_sdk/tools/code/linter/__init__.py +10 -8
- alita_sdk/tools/code/loaders/codesearcher.py +3 -2
- alita_sdk/tools/code/sonar/__init__.py +21 -13
- alita_sdk/tools/code_indexer_toolkit.py +199 -0
- alita_sdk/tools/confluence/__init__.py +22 -14
- alita_sdk/tools/confluence/api_wrapper.py +197 -58
- alita_sdk/tools/confluence/loader.py +14 -2
- alita_sdk/tools/custom_open_api/__init__.py +12 -5
- alita_sdk/tools/elastic/__init__.py +11 -8
- alita_sdk/tools/elitea_base.py +546 -64
- alita_sdk/tools/figma/__init__.py +60 -11
- alita_sdk/tools/figma/api_wrapper.py +1400 -167
- alita_sdk/tools/figma/figma_client.py +73 -0
- alita_sdk/tools/figma/toon_tools.py +2748 -0
- alita_sdk/tools/github/__init__.py +18 -17
- alita_sdk/tools/github/api_wrapper.py +9 -26
- alita_sdk/tools/github/github_client.py +81 -12
- alita_sdk/tools/github/schemas.py +2 -1
- alita_sdk/tools/github/tool.py +5 -1
- alita_sdk/tools/gitlab/__init__.py +19 -13
- alita_sdk/tools/gitlab/api_wrapper.py +256 -80
- alita_sdk/tools/gitlab_org/__init__.py +14 -10
- alita_sdk/tools/google/bigquery/__init__.py +14 -13
- alita_sdk/tools/google/bigquery/tool.py +5 -1
- alita_sdk/tools/google_places/__init__.py +21 -11
- alita_sdk/tools/jira/__init__.py +22 -11
- alita_sdk/tools/jira/api_wrapper.py +315 -168
- alita_sdk/tools/keycloak/__init__.py +11 -8
- alita_sdk/tools/localgit/__init__.py +9 -3
- alita_sdk/tools/localgit/local_git.py +62 -54
- alita_sdk/tools/localgit/tool.py +5 -1
- alita_sdk/tools/memory/__init__.py +38 -14
- alita_sdk/tools/non_code_indexer_toolkit.py +7 -2
- alita_sdk/tools/ocr/__init__.py +11 -8
- alita_sdk/tools/openapi/__init__.py +491 -106
- alita_sdk/tools/openapi/api_wrapper.py +1357 -0
- alita_sdk/tools/openapi/tool.py +20 -0
- alita_sdk/tools/pandas/__init__.py +20 -12
- alita_sdk/tools/pandas/api_wrapper.py +40 -45
- alita_sdk/tools/pandas/dataframe/generator/base.py +3 -1
- alita_sdk/tools/postman/__init__.py +11 -11
- alita_sdk/tools/postman/api_wrapper.py +19 -8
- alita_sdk/tools/postman/postman_analysis.py +8 -1
- alita_sdk/tools/pptx/__init__.py +11 -10
- alita_sdk/tools/qtest/__init__.py +22 -14
- alita_sdk/tools/qtest/api_wrapper.py +1784 -88
- alita_sdk/tools/rally/__init__.py +13 -10
- alita_sdk/tools/report_portal/__init__.py +23 -16
- alita_sdk/tools/salesforce/__init__.py +22 -16
- alita_sdk/tools/servicenow/__init__.py +21 -16
- alita_sdk/tools/servicenow/api_wrapper.py +1 -1
- alita_sdk/tools/sharepoint/__init__.py +17 -14
- alita_sdk/tools/sharepoint/api_wrapper.py +179 -39
- alita_sdk/tools/sharepoint/authorization_helper.py +191 -1
- alita_sdk/tools/sharepoint/utils.py +8 -2
- alita_sdk/tools/slack/__init__.py +13 -8
- alita_sdk/tools/sql/__init__.py +22 -19
- alita_sdk/tools/sql/api_wrapper.py +71 -23
- alita_sdk/tools/testio/__init__.py +21 -13
- alita_sdk/tools/testrail/__init__.py +13 -11
- alita_sdk/tools/testrail/api_wrapper.py +214 -46
- alita_sdk/tools/utils/__init__.py +28 -4
- alita_sdk/tools/utils/content_parser.py +241 -55
- alita_sdk/tools/utils/text_operations.py +254 -0
- alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +83 -27
- alita_sdk/tools/xray/__init__.py +18 -14
- alita_sdk/tools/xray/api_wrapper.py +58 -113
- alita_sdk/tools/yagmail/__init__.py +9 -3
- alita_sdk/tools/zephyr/__init__.py +12 -7
- alita_sdk/tools/zephyr_enterprise/__init__.py +16 -9
- alita_sdk/tools/zephyr_enterprise/api_wrapper.py +30 -15
- alita_sdk/tools/zephyr_essential/__init__.py +16 -10
- alita_sdk/tools/zephyr_essential/api_wrapper.py +297 -54
- alita_sdk/tools/zephyr_essential/client.py +6 -4
- alita_sdk/tools/zephyr_scale/__init__.py +13 -8
- alita_sdk/tools/zephyr_scale/api_wrapper.py +39 -31
- alita_sdk/tools/zephyr_squad/__init__.py +12 -7
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/METADATA +184 -37
- alita_sdk-0.3.584.dist-info/RECORD +452 -0
- alita_sdk-0.3.584.dist-info/entry_points.txt +2 -0
- alita_sdk/tools/bitbucket/tools.py +0 -304
- alita_sdk-0.3.257.dist-info/RECORD +0 -343
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.257.dist-info → alita_sdk-0.3.584.dist-info}/top_level.txt +0 -0
alita_sdk/runtime/tools/loop.py
CHANGED
|
@@ -102,7 +102,9 @@ Input Data:
|
|
|
102
102
|
logger.debug(f"LoopNode input: {predict_input}")
|
|
103
103
|
completion = self.client.invoke(predict_input, config=config)
|
|
104
104
|
logger.debug(f"LoopNode pure output: {completion}")
|
|
105
|
-
|
|
105
|
+
from ..langchain.utils import extract_text_from_completion
|
|
106
|
+
content_text = extract_text_from_completion(completion)
|
|
107
|
+
loop_data = _old_extract_json(content_text.strip())
|
|
106
108
|
logger.debug(f"LoopNode output: {loop_data}")
|
|
107
109
|
if self.return_type == "str":
|
|
108
110
|
accumulated_response = ''
|
|
@@ -93,7 +93,9 @@ Answer must be JSON only extractable by JSON.LOADS."""
|
|
|
93
93
|
else:
|
|
94
94
|
input_[-1].content += self.unstructured_output
|
|
95
95
|
completion = self.client.invoke(input_, config=config)
|
|
96
|
-
|
|
96
|
+
from ..langchain.utils import extract_text_from_completion
|
|
97
|
+
content_text = extract_text_from_completion(completion)
|
|
98
|
+
result = _extract_json(content_text.strip())
|
|
97
99
|
try:
|
|
98
100
|
tool_result: dict | List[dict] = self.tool.invoke(result, config=config, kwargs=kwargs)
|
|
99
101
|
dispatch_custom_event(
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Server Inspection Tool.
|
|
3
|
+
Allows inspecting available tools, prompts, and resources on an MCP server.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Type, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from langchain_core.tools import BaseTool
|
|
13
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
14
|
+
import aiohttp
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class McpInspectInput(BaseModel):
|
|
20
|
+
"""Input schema for MCP server inspection tool."""
|
|
21
|
+
|
|
22
|
+
resource_type: str = Field(
|
|
23
|
+
default="all",
|
|
24
|
+
description="What to inspect: 'tools', 'prompts', 'resources', or 'all'"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class McpInspectTool(BaseTool):
|
|
29
|
+
"""Tool for inspecting available tools, prompts, and resources on an MCP server."""
|
|
30
|
+
|
|
31
|
+
name: str = "mcp_inspect"
|
|
32
|
+
description: str = "List available tools, prompts, and resources from the MCP server"
|
|
33
|
+
args_schema: Type[BaseModel] = McpInspectInput
|
|
34
|
+
return_type: str = "str"
|
|
35
|
+
|
|
36
|
+
# MCP server connection details
|
|
37
|
+
server_name: str = Field(..., description="Name of the MCP server")
|
|
38
|
+
server_url: str = Field(..., description="URL of the MCP server")
|
|
39
|
+
server_headers: Optional[Dict[str, str]] = Field(default=None, description="HTTP headers for authentication")
|
|
40
|
+
timeout: int = Field(default=30, description="Request timeout in seconds")
|
|
41
|
+
|
|
42
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
43
|
+
|
|
44
|
+
def __getstate__(self):
|
|
45
|
+
"""Custom serialization for pickle compatibility."""
|
|
46
|
+
state = self.__dict__.copy()
|
|
47
|
+
# Convert headers dict to regular dict to avoid any reference issues
|
|
48
|
+
if 'server_headers' in state and state['server_headers'] is not None:
|
|
49
|
+
state['server_headers'] = dict(state['server_headers'])
|
|
50
|
+
return state
|
|
51
|
+
|
|
52
|
+
def __setstate__(self, state):
|
|
53
|
+
"""Custom deserialization for pickle compatibility."""
|
|
54
|
+
# Initialize Pydantic internal attributes if needed
|
|
55
|
+
if '__pydantic_fields_set__' not in state:
|
|
56
|
+
state['__pydantic_fields_set__'] = set(state.keys())
|
|
57
|
+
if '__pydantic_extra__' not in state:
|
|
58
|
+
state['__pydantic_extra__'] = None
|
|
59
|
+
if '__pydantic_private__' not in state:
|
|
60
|
+
state['__pydantic_private__'] = None
|
|
61
|
+
|
|
62
|
+
# Update object state
|
|
63
|
+
self.__dict__.update(state)
|
|
64
|
+
|
|
65
|
+
def _run(self, resource_type: str = "all") -> str:
|
|
66
|
+
"""Inspect the MCP server for available resources."""
|
|
67
|
+
try:
|
|
68
|
+
# Always create a new event loop for sync context
|
|
69
|
+
# This avoids issues with existing event loops in threads
|
|
70
|
+
import concurrent.futures
|
|
71
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
72
|
+
future = executor.submit(self._run_in_new_loop, resource_type)
|
|
73
|
+
return future.result(timeout=self.timeout)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.error(f"Error inspecting MCP server '{self.server_name}': {e}")
|
|
76
|
+
return f"Error inspecting MCP server: {e}"
|
|
77
|
+
|
|
78
|
+
def _run_in_new_loop(self, resource_type: str) -> str:
|
|
79
|
+
"""Run the async inspection in a new event loop."""
|
|
80
|
+
return asyncio.run(self._inspect_server(resource_type))
|
|
81
|
+
|
|
82
|
+
async def _inspect_server(self, resource_type: str) -> str:
|
|
83
|
+
"""Perform the actual MCP server inspection."""
|
|
84
|
+
results = {}
|
|
85
|
+
|
|
86
|
+
# Determine what to inspect
|
|
87
|
+
inspect_tools = resource_type in ["all", "tools"]
|
|
88
|
+
inspect_prompts = resource_type in ["all", "prompts"]
|
|
89
|
+
inspect_resources = resource_type in ["all", "resources"]
|
|
90
|
+
|
|
91
|
+
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self.timeout)) as session:
|
|
92
|
+
|
|
93
|
+
# List tools
|
|
94
|
+
if inspect_tools:
|
|
95
|
+
try:
|
|
96
|
+
tools = await self._list_tools(session)
|
|
97
|
+
results["tools"] = tools
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.warning(f"Failed to list tools from {self.server_name}: {e}")
|
|
100
|
+
results["tools"] = {"error": str(e)}
|
|
101
|
+
|
|
102
|
+
# List prompts
|
|
103
|
+
if inspect_prompts:
|
|
104
|
+
try:
|
|
105
|
+
prompts = await self._list_prompts(session)
|
|
106
|
+
results["prompts"] = prompts
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.warning(f"Failed to list prompts from {self.server_name}: {e}")
|
|
109
|
+
results["prompts"] = {"error": str(e)}
|
|
110
|
+
|
|
111
|
+
# List resources
|
|
112
|
+
if inspect_resources:
|
|
113
|
+
try:
|
|
114
|
+
resources = await self._list_resources(session)
|
|
115
|
+
results["resources"] = resources
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.warning(f"Failed to list resources from {self.server_name}: {e}")
|
|
118
|
+
results["resources"] = {"error": str(e)}
|
|
119
|
+
|
|
120
|
+
return self._format_results(results, resource_type)
|
|
121
|
+
|
|
122
|
+
def _parse_sse(self, text: str) -> Dict[str, Any]:
|
|
123
|
+
"""Parse Server-Sent Events (SSE) format response."""
|
|
124
|
+
for line in text.split('\n'):
|
|
125
|
+
line = line.strip()
|
|
126
|
+
if line.startswith('data:'):
|
|
127
|
+
json_str = line[5:].strip()
|
|
128
|
+
return json.loads(json_str)
|
|
129
|
+
raise ValueError("No data found in SSE response")
|
|
130
|
+
|
|
131
|
+
async def _list_tools(self, session: aiohttp.ClientSession) -> Dict[str, Any]:
|
|
132
|
+
"""List available tools from the MCP server."""
|
|
133
|
+
request = {
|
|
134
|
+
"jsonrpc": "2.0",
|
|
135
|
+
"id": f"list_tools_{int(time.time())}",
|
|
136
|
+
"method": "tools/list",
|
|
137
|
+
"params": {}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
headers = {
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
"Accept": "application/json, text/event-stream",
|
|
143
|
+
**self.server_headers
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async with session.post(self.server_url, json=request, headers=headers) as response:
|
|
147
|
+
if response.status != 200:
|
|
148
|
+
raise Exception(f"HTTP {response.status}: {await response.text()}")
|
|
149
|
+
|
|
150
|
+
# Handle both JSON and SSE responses
|
|
151
|
+
content_type = response.headers.get('Content-Type', '')
|
|
152
|
+
if 'text/event-stream' in content_type:
|
|
153
|
+
# Parse SSE format
|
|
154
|
+
text = await response.text()
|
|
155
|
+
data = self._parse_sse(text)
|
|
156
|
+
else:
|
|
157
|
+
data = await response.json()
|
|
158
|
+
|
|
159
|
+
if "error" in data:
|
|
160
|
+
raise Exception(f"MCP Error: {data['error']}")
|
|
161
|
+
|
|
162
|
+
return data.get("result", {})
|
|
163
|
+
|
|
164
|
+
async def _list_prompts(self, session: aiohttp.ClientSession) -> Dict[str, Any]:
|
|
165
|
+
"""List available prompts from the MCP server."""
|
|
166
|
+
request = {
|
|
167
|
+
"jsonrpc": "2.0",
|
|
168
|
+
"id": f"list_prompts_{int(time.time())}",
|
|
169
|
+
"method": "prompts/list",
|
|
170
|
+
"params": {}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
headers = {
|
|
174
|
+
"Content-Type": "application/json",
|
|
175
|
+
"Accept": "application/json, text/event-stream",
|
|
176
|
+
**self.server_headers
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async with session.post(self.server_url, json=request, headers=headers) as response:
|
|
180
|
+
if response.status != 200:
|
|
181
|
+
raise Exception(f"HTTP {response.status}: {await response.text()}")
|
|
182
|
+
|
|
183
|
+
# Handle both JSON and SSE responses
|
|
184
|
+
content_type = response.headers.get('Content-Type', '')
|
|
185
|
+
if 'text/event-stream' in content_type:
|
|
186
|
+
text = await response.text()
|
|
187
|
+
data = self._parse_sse(text)
|
|
188
|
+
else:
|
|
189
|
+
data = await response.json()
|
|
190
|
+
|
|
191
|
+
if "error" in data:
|
|
192
|
+
raise Exception(f"MCP Error: {data['error']}")
|
|
193
|
+
|
|
194
|
+
return data.get("result", {})
|
|
195
|
+
|
|
196
|
+
async def _list_resources(self, session: aiohttp.ClientSession) -> Dict[str, Any]:
|
|
197
|
+
"""List available resources from the MCP server."""
|
|
198
|
+
request = {
|
|
199
|
+
"jsonrpc": "2.0",
|
|
200
|
+
"id": f"list_resources_{int(time.time())}",
|
|
201
|
+
"method": "resources/list",
|
|
202
|
+
"params": {}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
headers = {
|
|
206
|
+
"Content-Type": "application/json",
|
|
207
|
+
"Accept": "application/json, text/event-stream",
|
|
208
|
+
**self.server_headers
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async with session.post(self.server_url, json=request, headers=headers) as response:
|
|
212
|
+
if response.status != 200:
|
|
213
|
+
raise Exception(f"HTTP {response.status}: {await response.text()}")
|
|
214
|
+
|
|
215
|
+
# Handle both JSON and SSE responses
|
|
216
|
+
content_type = response.headers.get('Content-Type', '')
|
|
217
|
+
if 'text/event-stream' in content_type:
|
|
218
|
+
text = await response.text()
|
|
219
|
+
data = self._parse_sse(text)
|
|
220
|
+
else:
|
|
221
|
+
data = await response.json()
|
|
222
|
+
|
|
223
|
+
if "error" in data:
|
|
224
|
+
raise Exception(f"MCP Error: {data['error']}")
|
|
225
|
+
|
|
226
|
+
return data.get("result", {})
|
|
227
|
+
|
|
228
|
+
def _format_results(self, results: Dict[str, Any], resource_type: str) -> str:
|
|
229
|
+
"""Format the inspection results for display."""
|
|
230
|
+
output_lines = [f"=== MCP Server Inspection: {self.server_name} ==="]
|
|
231
|
+
output_lines.append(f"Server URL: {self.server_url}")
|
|
232
|
+
output_lines.append("")
|
|
233
|
+
|
|
234
|
+
# Format tools
|
|
235
|
+
if "tools" in results:
|
|
236
|
+
if "error" in results["tools"]:
|
|
237
|
+
output_lines.append(f"❌ TOOLS: Error - {results['tools']['error']}")
|
|
238
|
+
else:
|
|
239
|
+
tools = results["tools"].get("tools", [])
|
|
240
|
+
output_lines.append(f"🔧 TOOLS ({len(tools)} available):")
|
|
241
|
+
if tools:
|
|
242
|
+
for tool in tools:
|
|
243
|
+
name = tool.get("name", "Unknown")
|
|
244
|
+
desc = tool.get("description", "No description")
|
|
245
|
+
output_lines.append(f" • {name}: {desc}")
|
|
246
|
+
else:
|
|
247
|
+
output_lines.append(" (No tools available)")
|
|
248
|
+
output_lines.append("")
|
|
249
|
+
|
|
250
|
+
# Format prompts
|
|
251
|
+
if "prompts" in results:
|
|
252
|
+
if "error" in results["prompts"]:
|
|
253
|
+
output_lines.append(f"❌ PROMPTS: Error - {results['prompts']['error']}")
|
|
254
|
+
else:
|
|
255
|
+
prompts = results["prompts"].get("prompts", [])
|
|
256
|
+
output_lines.append(f"💬 PROMPTS ({len(prompts)} available):")
|
|
257
|
+
if prompts:
|
|
258
|
+
for prompt in prompts:
|
|
259
|
+
name = prompt.get("name", "Unknown")
|
|
260
|
+
desc = prompt.get("description", "No description")
|
|
261
|
+
output_lines.append(f" • {name}: {desc}")
|
|
262
|
+
else:
|
|
263
|
+
output_lines.append(" (No prompts available)")
|
|
264
|
+
output_lines.append("")
|
|
265
|
+
|
|
266
|
+
# Format resources
|
|
267
|
+
if "resources" in results:
|
|
268
|
+
if "error" in results["resources"]:
|
|
269
|
+
output_lines.append(f"❌ RESOURCES: Error - {results['resources']['error']}")
|
|
270
|
+
else:
|
|
271
|
+
resources = results["resources"].get("resources", [])
|
|
272
|
+
output_lines.append(f"📁 RESOURCES ({len(resources)} available):")
|
|
273
|
+
if resources:
|
|
274
|
+
for resource in resources:
|
|
275
|
+
uri = resource.get("uri", "Unknown")
|
|
276
|
+
name = resource.get("name", uri)
|
|
277
|
+
desc = resource.get("description", "No description")
|
|
278
|
+
output_lines.append(f" • {name}: {desc}")
|
|
279
|
+
output_lines.append(f" URI: {uri}")
|
|
280
|
+
else:
|
|
281
|
+
output_lines.append(" (No resources available)")
|
|
282
|
+
output_lines.append("")
|
|
283
|
+
|
|
284
|
+
return "\n".join(output_lines)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Remote Tool for direct HTTP/SSE invocation.
|
|
3
|
+
This tool is used for remote MCP servers accessed via HTTP/SSE.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
from .mcp_server_tool import McpServerTool
|
|
15
|
+
from pydantic import Field
|
|
16
|
+
from ..utils.mcp_oauth import (
|
|
17
|
+
McpAuthorizationRequired,
|
|
18
|
+
canonical_resource,
|
|
19
|
+
extract_resource_metadata_url,
|
|
20
|
+
fetch_resource_metadata_async,
|
|
21
|
+
infer_authorization_servers_from_realm,
|
|
22
|
+
)
|
|
23
|
+
from ..utils.mcp_client import McpClient
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# Global registry to store MCP tool session metadata by tool name
|
|
28
|
+
# This is used to pass session info to callbacks since LangChain's serialization doesn't include all fields
|
|
29
|
+
MCP_TOOL_SESSION_REGISTRY: Dict[str, Dict[str, Any]] = {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class McpRemoteTool(McpServerTool):
|
|
33
|
+
"""
|
|
34
|
+
Tool for invoking remote MCP server tools via HTTP/SSE.
|
|
35
|
+
Extends McpServerTool and overrides _run to use direct HTTP calls instead of client.mcp_tool_call.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# Remote MCP connection details
|
|
39
|
+
server_url: str = Field(..., description="URL of the remote MCP server")
|
|
40
|
+
server_headers: Optional[Dict[str, str]] = Field(default=None, description="HTTP headers for authentication")
|
|
41
|
+
original_tool_name: Optional[str] = Field(default=None, description="Original tool name from MCP server (before optimization)")
|
|
42
|
+
is_prompt: bool = False # Flag to indicate if this is a prompt tool
|
|
43
|
+
prompt_name: Optional[str] = None # Original prompt name if this is a prompt
|
|
44
|
+
session_id: Optional[str] = Field(default=None, description="MCP session ID for stateful SSE servers")
|
|
45
|
+
|
|
46
|
+
def model_post_init(self, __context: Any) -> None:
|
|
47
|
+
"""Update metadata with session info after model initialization."""
|
|
48
|
+
super().model_post_init(__context)
|
|
49
|
+
self._update_metadata_with_session()
|
|
50
|
+
self._register_session_metadata()
|
|
51
|
+
|
|
52
|
+
def _update_metadata_with_session(self):
|
|
53
|
+
"""Update the metadata dict with current session information."""
|
|
54
|
+
if self.session_id:
|
|
55
|
+
if self.metadata is None:
|
|
56
|
+
self.metadata = {}
|
|
57
|
+
self.metadata.update({
|
|
58
|
+
'mcp_session_id': self.session_id,
|
|
59
|
+
'mcp_server_url': canonical_resource(self.server_url)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
def _register_session_metadata(self):
|
|
63
|
+
"""Register session metadata in global registry for callback access."""
|
|
64
|
+
if self.session_id and self.server_url:
|
|
65
|
+
MCP_TOOL_SESSION_REGISTRY[self.name] = {
|
|
66
|
+
'mcp_session_id': self.session_id,
|
|
67
|
+
'mcp_server_url': canonical_resource(self.server_url)
|
|
68
|
+
}
|
|
69
|
+
logger.debug(f"[MCP] Registered session metadata for tool '{self.name}': session={self.session_id}")
|
|
70
|
+
|
|
71
|
+
def __getstate__(self):
|
|
72
|
+
"""Custom serialization for pickle compatibility."""
|
|
73
|
+
state = super().__getstate__()
|
|
74
|
+
# Ensure headers are serializable
|
|
75
|
+
if 'server_headers' in state and state['server_headers'] is not None:
|
|
76
|
+
state['server_headers'] = dict(state['server_headers'])
|
|
77
|
+
return state
|
|
78
|
+
|
|
79
|
+
def _run(self, *args, **kwargs):
|
|
80
|
+
"""
|
|
81
|
+
Execute the MCP tool via direct HTTP/SSE call to the remote server.
|
|
82
|
+
Overrides the parent method to avoid using client.mcp_tool_call.
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
# Always create a new event loop for sync context
|
|
86
|
+
with ThreadPoolExecutor() as executor:
|
|
87
|
+
future = executor.submit(self._run_in_new_loop, kwargs)
|
|
88
|
+
return future.result(timeout=self.tool_timeout_sec)
|
|
89
|
+
except McpAuthorizationRequired:
|
|
90
|
+
# Bubble up so LangChain can surface a tool error with useful metadata
|
|
91
|
+
raise
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.error(f"Error executing remote MCP tool '{self.name}': {e}")
|
|
94
|
+
return f"Error executing tool: {e}"
|
|
95
|
+
|
|
96
|
+
def _run_in_new_loop(self, kwargs: Dict[str, Any]) -> str:
|
|
97
|
+
"""Run the async tool invocation in a new event loop."""
|
|
98
|
+
return asyncio.run(self._execute_remote_tool(kwargs))
|
|
99
|
+
|
|
100
|
+
async def _execute_remote_tool(self, kwargs: Dict[str, Any]) -> str:
|
|
101
|
+
"""Execute the actual remote MCP tool call using SSE client."""
|
|
102
|
+
|
|
103
|
+
# Check for session_id requirement
|
|
104
|
+
if not self.session_id:
|
|
105
|
+
logger.error(f"[MCP Session] Missing session_id for tool '{self.name}'")
|
|
106
|
+
raise Exception("sessionId required. Frontend must generate UUID and send with mcp_tokens.")
|
|
107
|
+
|
|
108
|
+
# Use the original tool name from discovery for MCP server invocation
|
|
109
|
+
tool_name_for_server = self.original_tool_name
|
|
110
|
+
if not tool_name_for_server:
|
|
111
|
+
tool_name_for_server = self.name
|
|
112
|
+
logger.warning(f"original_tool_name not set for '{self.name}', using: {tool_name_for_server}")
|
|
113
|
+
|
|
114
|
+
logger.info(f"[MCP] Executing tool '{tool_name_for_server}' with session {self.session_id}")
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
# Prepare headers
|
|
118
|
+
headers = {}
|
|
119
|
+
if self.server_headers:
|
|
120
|
+
headers.update(self.server_headers)
|
|
121
|
+
|
|
122
|
+
# Create unified MCP client (auto-detects transport)
|
|
123
|
+
client = McpClient(
|
|
124
|
+
url=self.server_url,
|
|
125
|
+
session_id=self.session_id,
|
|
126
|
+
headers=headers,
|
|
127
|
+
timeout=self.tool_timeout_sec
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Execute tool call (client auto-detects SSE vs Streamable HTTP)
|
|
131
|
+
async with client:
|
|
132
|
+
await client.initialize()
|
|
133
|
+
result = await client.call_tool(tool_name_for_server, kwargs)
|
|
134
|
+
|
|
135
|
+
# Format the result
|
|
136
|
+
if isinstance(result, dict):
|
|
137
|
+
# Check for content array (common in MCP responses)
|
|
138
|
+
if "content" in result:
|
|
139
|
+
content_items = result["content"]
|
|
140
|
+
if isinstance(content_items, list):
|
|
141
|
+
# Extract text from content items
|
|
142
|
+
text_parts = []
|
|
143
|
+
for item in content_items:
|
|
144
|
+
if isinstance(item, dict):
|
|
145
|
+
if item.get("type") == "text" and "text" in item:
|
|
146
|
+
text_parts.append(item["text"])
|
|
147
|
+
elif "text" in item:
|
|
148
|
+
text_parts.append(item["text"])
|
|
149
|
+
else:
|
|
150
|
+
text_parts.append(json.dumps(item))
|
|
151
|
+
else:
|
|
152
|
+
text_parts.append(str(item))
|
|
153
|
+
return "\n".join(text_parts)
|
|
154
|
+
|
|
155
|
+
# Return formatted JSON if no content field
|
|
156
|
+
return json.dumps(result, indent=2)
|
|
157
|
+
|
|
158
|
+
# Return as string for other types
|
|
159
|
+
return str(result)
|
|
160
|
+
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.error(f"[MCP] Tool execution failed: {e}", exc_info=True)
|
|
163
|
+
raise
|
|
164
|
+
|
|
165
|
+
def _parse_sse(self, text: str) -> Dict[str, Any]:
|
|
166
|
+
"""Parse Server-Sent Events (SSE) format response."""
|
|
167
|
+
for line in text.split('\n'):
|
|
168
|
+
line = line.strip()
|
|
169
|
+
if line.startswith('data:'):
|
|
170
|
+
json_str = line[5:].strip()
|
|
171
|
+
return json.loads(json_str)
|
|
172
|
+
raise ValueError("No data found in SSE response")
|
|
173
|
+
|
|
174
|
+
def get_session_metadata(self) -> dict:
|
|
175
|
+
"""Return session metadata to be included in tool responses."""
|
|
176
|
+
if self.session_id:
|
|
177
|
+
return {
|
|
178
|
+
'mcp_session_id': self.session_id,
|
|
179
|
+
'mcp_server_url': canonical_resource(self.server_url)
|
|
180
|
+
}
|
|
181
|
+
return {}
|
|
@@ -3,7 +3,7 @@ from logging import getLogger
|
|
|
3
3
|
from typing import Any, Type, Literal, Optional, Union, List
|
|
4
4
|
|
|
5
5
|
from langchain_core.tools import BaseTool
|
|
6
|
-
from pydantic import BaseModel, Field, create_model, EmailStr, constr
|
|
6
|
+
from pydantic import BaseModel, Field, create_model, EmailStr, constr, ConfigDict
|
|
7
7
|
|
|
8
8
|
logger = getLogger(__name__)
|
|
9
9
|
|
|
@@ -17,6 +17,7 @@ class McpServerTool(BaseTool):
|
|
|
17
17
|
server: str
|
|
18
18
|
tool_timeout_sec: int = 60
|
|
19
19
|
|
|
20
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
20
21
|
|
|
21
22
|
@staticmethod
|
|
22
23
|
def create_pydantic_model_from_schema(schema: dict, model_name: str = "ArgsSchema"):
|
|
@@ -88,6 +89,7 @@ class McpServerTool(BaseTool):
|
|
|
88
89
|
return create_model(model_name, **fields)
|
|
89
90
|
|
|
90
91
|
def _run(self, *args, **kwargs):
|
|
92
|
+
# Use the tool name directly (no prefix extraction needed)
|
|
91
93
|
call_data = {
|
|
92
94
|
"server": self.server,
|
|
93
95
|
"tool_timeout_sec": self.tool_timeout_sec,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Planning tools for runtime agents.
|
|
3
|
+
|
|
4
|
+
Provides plan management for multi-step task execution with progress tracking.
|
|
5
|
+
Supports two storage backends:
|
|
6
|
+
1. PostgreSQL - when connection_string is provided (production/indexer_worker)
|
|
7
|
+
2. Filesystem - when no connection string (local CLI usage)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .wrapper import (
|
|
11
|
+
PlanningWrapper,
|
|
12
|
+
PlanStep,
|
|
13
|
+
PlanState,
|
|
14
|
+
FilesystemStorage,
|
|
15
|
+
PostgresStorage,
|
|
16
|
+
)
|
|
17
|
+
from .models import (
|
|
18
|
+
AgentPlan,
|
|
19
|
+
PlanStatus,
|
|
20
|
+
ensure_plan_tables,
|
|
21
|
+
delete_plan_by_conversation_id,
|
|
22
|
+
cleanup_on_graceful_completion
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"PlanningWrapper",
|
|
27
|
+
"PlanStep",
|
|
28
|
+
"PlanState",
|
|
29
|
+
"FilesystemStorage",
|
|
30
|
+
"PostgresStorage",
|
|
31
|
+
"AgentPlan",
|
|
32
|
+
"PlanStatus",
|
|
33
|
+
"ensure_plan_tables",
|
|
34
|
+
"delete_plan_by_conversation_id",
|
|
35
|
+
"cleanup_on_graceful_completion",
|
|
36
|
+
]
|