alita-sdk 0.3.497__py3-none-any.whl → 0.3.516__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/inventory.py +12 -195
- alita_sdk/community/inventory/__init__.py +12 -0
- alita_sdk/community/inventory/toolkit.py +9 -5
- alita_sdk/community/inventory/toolkit_utils.py +176 -0
- alita_sdk/configurations/ado.py +144 -0
- alita_sdk/configurations/confluence.py +76 -42
- alita_sdk/configurations/figma.py +76 -0
- alita_sdk/configurations/gitlab.py +2 -0
- alita_sdk/configurations/qtest.py +72 -1
- alita_sdk/configurations/report_portal.py +96 -0
- alita_sdk/configurations/sharepoint.py +148 -0
- alita_sdk/configurations/testio.py +83 -0
- alita_sdk/runtime/clients/artifact.py +2 -2
- alita_sdk/runtime/clients/client.py +24 -19
- alita_sdk/runtime/clients/sandbox_client.py +14 -0
- alita_sdk/runtime/langchain/assistant.py +64 -23
- alita_sdk/runtime/langchain/constants.py +270 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
- alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +2 -1
- alita_sdk/runtime/langchain/document_loaders/constants.py +2 -1
- alita_sdk/runtime/langchain/langraph_agent.py +8 -9
- alita_sdk/runtime/langchain/utils.py +6 -1
- alita_sdk/runtime/toolkits/artifact.py +14 -5
- alita_sdk/runtime/toolkits/datasource.py +13 -6
- alita_sdk/runtime/toolkits/mcp.py +26 -157
- alita_sdk/runtime/toolkits/planning.py +10 -5
- alita_sdk/runtime/toolkits/tools.py +23 -7
- alita_sdk/runtime/toolkits/vectorstore.py +11 -5
- alita_sdk/runtime/tools/artifact.py +139 -6
- alita_sdk/runtime/tools/llm.py +20 -10
- alita_sdk/runtime/tools/mcp_remote_tool.py +2 -3
- alita_sdk/runtime/tools/mcp_server_tool.py +2 -4
- alita_sdk/runtime/utils/AlitaCallback.py +30 -1
- alita_sdk/runtime/utils/mcp_client.py +33 -6
- alita_sdk/runtime/utils/mcp_oauth.py +125 -8
- alita_sdk/runtime/utils/mcp_sse_client.py +35 -6
- alita_sdk/runtime/utils/utils.py +2 -0
- alita_sdk/tools/__init__.py +15 -0
- alita_sdk/tools/ado/repos/__init__.py +10 -12
- alita_sdk/tools/ado/test_plan/__init__.py +23 -8
- alita_sdk/tools/ado/wiki/__init__.py +24 -8
- alita_sdk/tools/ado/wiki/ado_wrapper.py +21 -7
- alita_sdk/tools/ado/work_item/__init__.py +24 -8
- alita_sdk/tools/advanced_jira_mining/__init__.py +10 -8
- alita_sdk/tools/aws/delta_lake/__init__.py +12 -9
- alita_sdk/tools/aws/delta_lake/tool.py +5 -1
- alita_sdk/tools/azure_ai/search/__init__.py +9 -7
- alita_sdk/tools/base/tool.py +5 -1
- alita_sdk/tools/base_indexer_toolkit.py +25 -0
- alita_sdk/tools/bitbucket/__init__.py +14 -10
- alita_sdk/tools/bitbucket/api_wrapper.py +50 -2
- alita_sdk/tools/browser/__init__.py +5 -4
- alita_sdk/tools/carrier/__init__.py +5 -6
- alita_sdk/tools/cloud/aws/__init__.py +9 -7
- alita_sdk/tools/cloud/azure/__init__.py +9 -7
- alita_sdk/tools/cloud/gcp/__init__.py +9 -7
- alita_sdk/tools/cloud/k8s/__init__.py +9 -7
- alita_sdk/tools/code/linter/__init__.py +9 -8
- alita_sdk/tools/code/sonar/__init__.py +9 -7
- alita_sdk/tools/confluence/__init__.py +15 -10
- alita_sdk/tools/custom_open_api/__init__.py +11 -5
- alita_sdk/tools/elastic/__init__.py +10 -8
- alita_sdk/tools/elitea_base.py +387 -9
- alita_sdk/tools/figma/__init__.py +8 -7
- alita_sdk/tools/github/__init__.py +12 -14
- alita_sdk/tools/github/github_client.py +68 -2
- alita_sdk/tools/github/tool.py +5 -1
- alita_sdk/tools/gitlab/__init__.py +14 -11
- alita_sdk/tools/gitlab/api_wrapper.py +81 -1
- alita_sdk/tools/gitlab_org/__init__.py +9 -8
- alita_sdk/tools/google/bigquery/__init__.py +12 -12
- alita_sdk/tools/google/bigquery/tool.py +5 -1
- alita_sdk/tools/google_places/__init__.py +9 -8
- alita_sdk/tools/jira/__init__.py +15 -10
- alita_sdk/tools/keycloak/__init__.py +10 -8
- alita_sdk/tools/localgit/__init__.py +8 -3
- alita_sdk/tools/localgit/local_git.py +62 -54
- alita_sdk/tools/localgit/tool.py +5 -1
- alita_sdk/tools/memory/__init__.py +11 -3
- alita_sdk/tools/ocr/__init__.py +10 -8
- alita_sdk/tools/openapi/__init__.py +6 -2
- alita_sdk/tools/pandas/__init__.py +9 -7
- alita_sdk/tools/postman/__init__.py +10 -11
- alita_sdk/tools/pptx/__init__.py +9 -9
- alita_sdk/tools/qtest/__init__.py +9 -8
- alita_sdk/tools/rally/__init__.py +9 -8
- alita_sdk/tools/report_portal/__init__.py +11 -9
- alita_sdk/tools/salesforce/__init__.py +9 -9
- alita_sdk/tools/servicenow/__init__.py +10 -8
- alita_sdk/tools/sharepoint/__init__.py +9 -8
- alita_sdk/tools/slack/__init__.py +8 -7
- alita_sdk/tools/sql/__init__.py +9 -8
- alita_sdk/tools/testio/__init__.py +9 -8
- alita_sdk/tools/testrail/__init__.py +10 -8
- alita_sdk/tools/utils/__init__.py +9 -4
- alita_sdk/tools/utils/text_operations.py +254 -0
- alita_sdk/tools/xray/__init__.py +10 -8
- alita_sdk/tools/yagmail/__init__.py +8 -3
- alita_sdk/tools/zephyr/__init__.py +8 -7
- alita_sdk/tools/zephyr_enterprise/__init__.py +10 -8
- alita_sdk/tools/zephyr_essential/__init__.py +9 -8
- alita_sdk/tools/zephyr_scale/__init__.py +9 -8
- alita_sdk/tools/zephyr_squad/__init__.py +9 -8
- {alita_sdk-0.3.497.dist-info → alita_sdk-0.3.516.dist-info}/METADATA +1 -1
- {alita_sdk-0.3.497.dist-info → alita_sdk-0.3.516.dist-info}/RECORD +109 -106
- {alita_sdk-0.3.497.dist-info → alita_sdk-0.3.516.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.497.dist-info → alita_sdk-0.3.516.dist-info}/entry_points.txt +0 -0
- {alita_sdk-0.3.497.dist-info → alita_sdk-0.3.516.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.497.dist-info → alita_sdk-0.3.516.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from .AlitaJSONLoader import AlitaJSONLoader
|
|
2
|
+
import json
|
|
3
|
+
from io import StringIO
|
|
4
|
+
from typing import List, Iterator
|
|
5
|
+
|
|
6
|
+
from langchain_core.documents import Document
|
|
7
|
+
from langchain_core.tools import ToolException
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AlitaJSONLinesLoader(AlitaJSONLoader):
|
|
11
|
+
"""Load local JSONL files (one JSON object per line) using AlitaJSONLoader behavior.
|
|
12
|
+
|
|
13
|
+
Behavior:
|
|
14
|
+
- Supports both `file_path` and `file_content` (bytes or file-like object), same as AlitaJSONLoader.
|
|
15
|
+
- Treats each non-empty line as an independent JSON object.
|
|
16
|
+
- Aggregates all parsed JSON objects into a list and feeds them through the same
|
|
17
|
+
RecursiveJsonSplitter-based chunking used by AlitaJSONLoader.lazy_load.
|
|
18
|
+
- Returns a list of Documents with chunked JSON content.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, **kwargs):
|
|
22
|
+
# Reuse AlitaJSONLoader initialization logic (file_path / file_content handling, encoding, etc.)
|
|
23
|
+
super().__init__(**kwargs)
|
|
24
|
+
|
|
25
|
+
def _iter_lines(self) -> Iterator[str]:
|
|
26
|
+
"""Yield lines from file_path or file_content, mirroring AlitaJSONLoader sources."""
|
|
27
|
+
# Prefer file_path if available
|
|
28
|
+
if hasattr(self, "file_path") and self.file_path:
|
|
29
|
+
with open(self.file_path, "r", encoding=self.encoding) as f:
|
|
30
|
+
for line in f:
|
|
31
|
+
yield line
|
|
32
|
+
# Fallback to file_content if available
|
|
33
|
+
elif hasattr(self, "file_content") and self.file_content:
|
|
34
|
+
# file_content may be bytes or a file-like object
|
|
35
|
+
if isinstance(self.file_content, (bytes, bytearray)):
|
|
36
|
+
text = self.file_content.decode(self.encoding)
|
|
37
|
+
for line in StringIO(text):
|
|
38
|
+
yield line
|
|
39
|
+
else:
|
|
40
|
+
# Assume it's a text file-like object positioned at the beginning
|
|
41
|
+
self.file_content.seek(0)
|
|
42
|
+
for line in self.file_content:
|
|
43
|
+
yield line
|
|
44
|
+
else:
|
|
45
|
+
raise ToolException("'file_path' or 'file_content' parameter should be provided.")
|
|
46
|
+
|
|
47
|
+
def load(self) -> List[Document]: # type: ignore[override]
|
|
48
|
+
"""Load JSONL content by delegating each non-empty line to AlitaJSONLoader.
|
|
49
|
+
|
|
50
|
+
For each non-empty line in the underlying source (file_path or file_content):
|
|
51
|
+
- Create a temporary AlitaJSONLoader instance with that line as file_content.
|
|
52
|
+
- Call lazy_load() on that instance to apply the same RecursiveJsonSplitter logic
|
|
53
|
+
as for a normal JSON file.
|
|
54
|
+
- Accumulate all Documents from all lines and return them as a single list.
|
|
55
|
+
"""
|
|
56
|
+
docs: List[Document] = []
|
|
57
|
+
|
|
58
|
+
for raw_line in self._iter_lines():
|
|
59
|
+
line = raw_line.strip()
|
|
60
|
+
if not line:
|
|
61
|
+
continue
|
|
62
|
+
try:
|
|
63
|
+
# Instantiate a per-line AlitaJSONLoader using the same configuration
|
|
64
|
+
line_loader = AlitaJSONLoader(
|
|
65
|
+
file_content=line,
|
|
66
|
+
file_name=getattr(self, "file_name", str(getattr(self, "file_path", "no_name"))),
|
|
67
|
+
encoding=self.encoding,
|
|
68
|
+
autodetect_encoding=self.autodetect_encoding,
|
|
69
|
+
max_tokens=self.max_tokens,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
for doc in line_loader.lazy_load():
|
|
73
|
+
docs.append(doc)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
raise ToolException(f"Error processing JSONL line: {line[:100]}... Error: {e}") from e
|
|
76
|
+
|
|
77
|
+
return docs
|
|
@@ -32,6 +32,8 @@ class AlitaJSONLoader(BaseLoader):
|
|
|
32
32
|
elif hasattr(self, 'file_content') and self.file_content:
|
|
33
33
|
if isinstance(self.file_content, bytes):
|
|
34
34
|
return json.loads(self.file_content.decode(self.encoding))
|
|
35
|
+
elif isinstance(self.file_content, str):
|
|
36
|
+
return json.loads(self.file_content)
|
|
35
37
|
else:
|
|
36
38
|
return json.load(self.file_content)
|
|
37
39
|
else:
|
|
@@ -45,7 +47,6 @@ class AlitaJSONLoader(BaseLoader):
|
|
|
45
47
|
try:
|
|
46
48
|
with open(self.file_path, encoding=encoding.encoding) as f:
|
|
47
49
|
return f.read()
|
|
48
|
-
break
|
|
49
50
|
except UnicodeDecodeError:
|
|
50
51
|
continue
|
|
51
52
|
elif hasattr(self, 'file_content') and self.file_content:
|
|
@@ -21,6 +21,7 @@ from .AlitaDocxMammothLoader import AlitaDocxMammothLoader
|
|
|
21
21
|
from .AlitaExcelLoader import AlitaExcelLoader
|
|
22
22
|
from .AlitaImageLoader import AlitaImageLoader
|
|
23
23
|
from .AlitaJSONLoader import AlitaJSONLoader
|
|
24
|
+
from .AlitaJSONLinesLoader import AlitaJSONLinesLoader
|
|
24
25
|
from .AlitaPDFLoader import AlitaPDFLoader
|
|
25
26
|
from .AlitaPowerPointLoader import AlitaPowerPointLoader
|
|
26
27
|
from .AlitaTextLoader import AlitaTextLoader
|
|
@@ -208,7 +209,7 @@ document_loaders_map = {
|
|
|
208
209
|
'allowed_to_override': DEFAULT_ALLOWED_BASE
|
|
209
210
|
},
|
|
210
211
|
'.jsonl': {
|
|
211
|
-
'class':
|
|
212
|
+
'class': AlitaJSONLinesLoader,
|
|
212
213
|
'mime_type': 'application/jsonl',
|
|
213
214
|
'is_multimodal_processing': False,
|
|
214
215
|
'kwargs': {},
|
|
@@ -30,7 +30,7 @@ from ..tools.loop import LoopNode
|
|
|
30
30
|
from ..tools.loop_output import LoopToolNode
|
|
31
31
|
from ..tools.tool import ToolNode
|
|
32
32
|
from ..utils.evaluate import EvaluateTemplate
|
|
33
|
-
from ..utils.utils import clean_string
|
|
33
|
+
from ..utils.utils import clean_string
|
|
34
34
|
from ..tools.router import RouterNode
|
|
35
35
|
|
|
36
36
|
logger = logging.getLogger(__name__)
|
|
@@ -191,7 +191,7 @@ Answer only with step name, no need to add descrip in case none of the steps are
|
|
|
191
191
|
additional_info = """### Additoinal info: """
|
|
192
192
|
additional_info += "{field}: {value}\n".format(field=field, value=state.get(field, ""))
|
|
193
193
|
decision_input.append(HumanMessage(
|
|
194
|
-
self.prompt.format(steps=self.steps, description=self.description, additional_info=additional_info)))
|
|
194
|
+
self.prompt.format(steps=self.steps, description=safe_format(self.description, state), additional_info=additional_info)))
|
|
195
195
|
completion = self.client.invoke(decision_input)
|
|
196
196
|
result = clean_string(completion.content.strip())
|
|
197
197
|
logger.info(f"Plan to transition to: {result}")
|
|
@@ -483,8 +483,7 @@ def create_graph(
|
|
|
483
483
|
node_id = clean_string(node['id'])
|
|
484
484
|
toolkit_name = node.get('toolkit_name')
|
|
485
485
|
tool_name = clean_string(node.get('tool', node_id))
|
|
486
|
-
|
|
487
|
-
tool_name = f"{clean_string(toolkit_name)}{TOOLKIT_SPLITTER}{tool_name}"
|
|
486
|
+
# Tool names are now clean (no prefix needed)
|
|
488
487
|
logger.info(f"Node: {node_id} : {node_type} - {tool_name}")
|
|
489
488
|
if node_type in ['function', 'toolkit', 'mcp', 'tool', 'loop', 'loop_from_tool', 'indexer', 'subgraph', 'pipeline', 'agent']:
|
|
490
489
|
if node_type == 'mcp' and tool_name not in [tool.name for tool in tools]:
|
|
@@ -550,8 +549,8 @@ def create_graph(
|
|
|
550
549
|
loop_toolkit_name = node.get('loop_toolkit_name')
|
|
551
550
|
loop_tool_name = node.get('loop_tool')
|
|
552
551
|
if (loop_toolkit_name and loop_tool_name) or loop_tool_name:
|
|
553
|
-
|
|
554
|
-
|
|
552
|
+
# Use clean tool name (no prefix)
|
|
553
|
+
loop_tool_name = clean_string(loop_tool_name)
|
|
555
554
|
for t in tools:
|
|
556
555
|
if t.name == loop_tool_name:
|
|
557
556
|
logger.debug(f"Loop tool discovered: {t}")
|
|
@@ -609,10 +608,10 @@ def create_graph(
|
|
|
609
608
|
tool_names = []
|
|
610
609
|
if isinstance(connected_tools, dict):
|
|
611
610
|
for toolkit, selected_tools in connected_tools.items():
|
|
612
|
-
|
|
613
|
-
|
|
611
|
+
# Add tool names directly (no prefix)
|
|
612
|
+
tool_names.extend(selected_tools)
|
|
614
613
|
elif isinstance(connected_tools, list):
|
|
615
|
-
#
|
|
614
|
+
# Use provided tool names as-is
|
|
616
615
|
tool_names = connected_tools
|
|
617
616
|
|
|
618
617
|
if tool_names:
|
|
@@ -208,7 +208,12 @@ def safe_format(template, mapping):
|
|
|
208
208
|
def create_pydantic_model(model_name: str, variables: dict[str, dict]):
|
|
209
209
|
fields = {}
|
|
210
210
|
for var_name, var_data in variables.items():
|
|
211
|
-
|
|
211
|
+
if 'default' in var_data:
|
|
212
|
+
# allow user to define if it is required or not
|
|
213
|
+
fields[var_name] = (parse_pydantic_type(var_data['type']),
|
|
214
|
+
Field(description=var_data.get('description', None), default=var_data.get('default')))
|
|
215
|
+
else:
|
|
216
|
+
fields[var_name] = (parse_pydantic_type(var_data['type']), Field(description=var_data.get('description', None)))
|
|
212
217
|
return create_model(model_name, **fields)
|
|
213
218
|
|
|
214
219
|
def parse_pydantic_type(type_name: str):
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import List, Any, Literal, Optional
|
|
2
2
|
|
|
3
|
-
from alita_sdk.tools.utils import clean_string,
|
|
3
|
+
from alita_sdk.tools.utils import clean_string, get_max_toolkit_length
|
|
4
|
+
from alita_sdk.tools.elitea_base import filter_missconfigured_index_tools
|
|
4
5
|
from langchain_community.agent_toolkits.base import BaseToolkit
|
|
5
6
|
from langchain_core.tools import BaseTool
|
|
6
7
|
from pydantic import create_model, BaseModel, ConfigDict, Field
|
|
@@ -40,26 +41,34 @@ class ArtifactToolkit(BaseToolkit):
|
|
|
40
41
|
)
|
|
41
42
|
|
|
42
43
|
@classmethod
|
|
44
|
+
@filter_missconfigured_index_tools
|
|
43
45
|
def get_toolkit(cls, client: Any, bucket: str, toolkit_name: Optional[str] = None, selected_tools: list[str] = [], **kwargs):
|
|
44
46
|
if selected_tools is None:
|
|
45
47
|
selected_tools = []
|
|
48
|
+
|
|
46
49
|
tools = []
|
|
47
50
|
wrapper_payload = {
|
|
48
51
|
**kwargs,
|
|
49
52
|
**(kwargs.get('pgvector_configuration') or {}),
|
|
50
53
|
}
|
|
51
54
|
artifact_wrapper = ArtifactWrapper(alita=client, bucket=bucket, **wrapper_payload)
|
|
52
|
-
|
|
55
|
+
# Use clean toolkit name for context (max 1000 chars in description)
|
|
56
|
+
toolkit_context = f" [Toolkit: {clean_string(toolkit_name, 0)}]" if toolkit_name else ''
|
|
53
57
|
available_tools = artifact_wrapper.get_available_tools()
|
|
54
58
|
for tool in available_tools:
|
|
55
59
|
if selected_tools:
|
|
56
60
|
if tool["name"] not in selected_tools:
|
|
57
61
|
continue
|
|
62
|
+
# Add toolkit context to description with character limit
|
|
63
|
+
description = tool["description"]
|
|
64
|
+
if toolkit_context and len(description + toolkit_context) <= 1000:
|
|
65
|
+
description = description + toolkit_context
|
|
58
66
|
tools.append(BaseAction(
|
|
59
67
|
api_wrapper=artifact_wrapper,
|
|
60
|
-
name=
|
|
61
|
-
description=
|
|
62
|
-
args_schema=tool["args_schema"]
|
|
68
|
+
name=tool["name"],
|
|
69
|
+
description=description,
|
|
70
|
+
args_schema=tool["args_schema"],
|
|
71
|
+
metadata={"toolkit_name": toolkit_name} if toolkit_name else {}
|
|
63
72
|
))
|
|
64
73
|
return cls(tools=tools)
|
|
65
74
|
|
|
@@ -3,7 +3,7 @@ from pydantic import create_model, BaseModel, Field
|
|
|
3
3
|
from langchain_community.agent_toolkits.base import BaseToolkit
|
|
4
4
|
from langchain_core.tools import BaseTool, ToolException
|
|
5
5
|
from ..tools.datasource import DatasourcePredict, DatasourceSearch, datasourceToolSchema
|
|
6
|
-
from alita_sdk.tools.utils import clean_string
|
|
6
|
+
from alita_sdk.tools.utils import clean_string
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class DatasourcesToolkit(BaseToolkit):
|
|
@@ -21,21 +21,28 @@ class DatasourcesToolkit(BaseToolkit):
|
|
|
21
21
|
@classmethod
|
|
22
22
|
def get_toolkit(cls, client: Any, datasource_ids: list[int], toolkit_name: Optional[str] = None, selected_tools: list[str] = []):
|
|
23
23
|
tools = []
|
|
24
|
-
|
|
24
|
+
# Use clean toolkit name for context (max 1000 chars in description)
|
|
25
|
+
toolkit_context = f" [Toolkit: {clean_string(toolkit_name)}]" if toolkit_name else ''
|
|
25
26
|
for datasource_id in datasource_ids:
|
|
26
27
|
datasource = client.datasource(datasource_id)
|
|
27
28
|
ds_name = clean_string(datasource.name)
|
|
28
29
|
if len(ds_name) == 0:
|
|
29
30
|
raise ToolException(f'Datasource with id {datasource_id} has incorrect name (i.e. special characters, etc.)')
|
|
30
31
|
if len(selected_tools) == 0 or 'chat' in selected_tools:
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
description = f'Search and summarize. {datasource.description}'
|
|
33
|
+
if toolkit_context and len(description + toolkit_context) <= 1000:
|
|
34
|
+
description = description + toolkit_context
|
|
35
|
+
tools.append(DatasourcePredict(name=f'chat',
|
|
36
|
+
description=description,
|
|
33
37
|
datasource=datasource,
|
|
34
38
|
args_schema=datasourceToolSchema,
|
|
35
39
|
return_type='str'))
|
|
36
40
|
if len(selected_tools) == 0 or 'search' in selected_tools:
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
description = f'Search return results. {datasource.description}'
|
|
42
|
+
if toolkit_context and len(description + toolkit_context) <= 1000:
|
|
43
|
+
description = description + toolkit_context
|
|
44
|
+
tools.append(DatasourceSearch(name=f'search',
|
|
45
|
+
description=description,
|
|
39
46
|
datasource=datasource,
|
|
40
47
|
args_schema=datasourceToolSchema,
|
|
41
48
|
return_type='str'))
|
|
@@ -15,7 +15,6 @@ from pydantic import BaseModel, ConfigDict, Field, SecretStr
|
|
|
15
15
|
from ..tools.mcp_server_tool import McpServerTool
|
|
16
16
|
from ..tools.mcp_remote_tool import McpRemoteTool
|
|
17
17
|
from ..tools.mcp_inspect_tool import McpInspectTool
|
|
18
|
-
from ...tools.utils import TOOLKIT_SPLITTER, clean_string
|
|
19
18
|
from ..models.mcp_models import McpConnectionConfig
|
|
20
19
|
from ..utils.mcp_client import McpClient
|
|
21
20
|
from ..utils.mcp_oauth import (
|
|
@@ -40,110 +39,6 @@ def safe_int(value, default):
|
|
|
40
39
|
logger.warning(f"Invalid integer value '{value}', using default {default}")
|
|
41
40
|
return default
|
|
42
41
|
|
|
43
|
-
def optimize_tool_name(prefix: str, tool_name: str, max_total_length: int = 64) -> str:
|
|
44
|
-
"""
|
|
45
|
-
Optimize tool name to fit within max_total_length while preserving meaning.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
prefix: The toolkit prefix (already cleaned)
|
|
49
|
-
tool_name: The original tool name
|
|
50
|
-
max_total_length: Maximum total length for the full tool name (default: 64)
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
Optimized full tool name in format: prefix___tool_name
|
|
54
|
-
"""
|
|
55
|
-
splitter = TOOLKIT_SPLITTER
|
|
56
|
-
splitter_len = len(splitter)
|
|
57
|
-
prefix_len = len(prefix)
|
|
58
|
-
|
|
59
|
-
# Calculate available space for tool name
|
|
60
|
-
available_space = max_total_length - prefix_len - splitter_len
|
|
61
|
-
|
|
62
|
-
if available_space <= 0:
|
|
63
|
-
logger.error(f"Prefix '{prefix}' is too long ({prefix_len} chars), cannot create valid tool name")
|
|
64
|
-
# Fallback: truncate prefix itself
|
|
65
|
-
prefix = prefix[:max_total_length - splitter_len - 10] # Leave 10 chars for tool name
|
|
66
|
-
available_space = max_total_length - len(prefix) - splitter_len
|
|
67
|
-
|
|
68
|
-
# If tool name fits, use it as-is
|
|
69
|
-
if len(tool_name) <= available_space:
|
|
70
|
-
return f'{prefix}{splitter}{tool_name}'
|
|
71
|
-
|
|
72
|
-
# Tool name is too long, need to optimize
|
|
73
|
-
logger.debug(f"Tool name '{tool_name}' is too long ({len(tool_name)} chars), optimizing to fit {available_space} chars")
|
|
74
|
-
|
|
75
|
-
# Split tool name into parts (handle camelCase, snake_case, and mixed)
|
|
76
|
-
# First, split by underscores and hyphens
|
|
77
|
-
parts = re.split(r'[_-]', tool_name)
|
|
78
|
-
|
|
79
|
-
# Further split camelCase within each part
|
|
80
|
-
all_parts = []
|
|
81
|
-
for part in parts:
|
|
82
|
-
# Insert underscore before uppercase letters (camelCase to snake_case)
|
|
83
|
-
snake_case_part = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', part)
|
|
84
|
-
all_parts.extend(snake_case_part.split('_'))
|
|
85
|
-
|
|
86
|
-
# Filter out empty parts
|
|
87
|
-
all_parts = [p for p in all_parts if p]
|
|
88
|
-
|
|
89
|
-
# Remove redundant prefix words (case-insensitive comparison)
|
|
90
|
-
# Only remove if prefix is meaningful (>= 3 chars) to avoid over-filtering
|
|
91
|
-
prefix_lower = prefix.lower()
|
|
92
|
-
filtered_parts = []
|
|
93
|
-
for part in all_parts:
|
|
94
|
-
part_lower = part.lower()
|
|
95
|
-
# Skip if this part contains the prefix or the prefix contains this part
|
|
96
|
-
# But only if both are meaningful (>= 3 chars)
|
|
97
|
-
should_remove = False
|
|
98
|
-
if len(prefix_lower) >= 3 and len(part_lower) >= 3:
|
|
99
|
-
if part_lower in prefix_lower or prefix_lower in part_lower:
|
|
100
|
-
should_remove = True
|
|
101
|
-
logger.debug(f"Removing redundant part '{part}' (matches prefix '{prefix}')")
|
|
102
|
-
|
|
103
|
-
if not should_remove:
|
|
104
|
-
filtered_parts.append(part)
|
|
105
|
-
|
|
106
|
-
# If we removed all parts, keep the original parts
|
|
107
|
-
if not filtered_parts:
|
|
108
|
-
filtered_parts = all_parts
|
|
109
|
-
|
|
110
|
-
# Reconstruct tool name with filtered parts
|
|
111
|
-
optimized_name = '_'.join(filtered_parts)
|
|
112
|
-
|
|
113
|
-
# If still too long, truncate intelligently
|
|
114
|
-
if len(optimized_name) > available_space:
|
|
115
|
-
# Strategy: Keep beginning and end, as they often contain the most important info
|
|
116
|
-
# For example: "projectalita_github_io_list_branches" -> "projectalita_list_branches"
|
|
117
|
-
|
|
118
|
-
# Try removing middle parts first
|
|
119
|
-
if len(filtered_parts) > 2:
|
|
120
|
-
# Keep first and last parts, remove middle
|
|
121
|
-
kept_parts = [filtered_parts[0], filtered_parts[-1]]
|
|
122
|
-
optimized_name = '_'.join(kept_parts)
|
|
123
|
-
|
|
124
|
-
# If still too long, add parts from the end until we run out of space
|
|
125
|
-
if len(optimized_name) <= available_space and len(filtered_parts) > 2:
|
|
126
|
-
for i in range(len(filtered_parts) - 2, 0, -1):
|
|
127
|
-
candidate = '_'.join([filtered_parts[0]] + filtered_parts[i:])
|
|
128
|
-
if len(candidate) <= available_space:
|
|
129
|
-
optimized_name = candidate
|
|
130
|
-
break
|
|
131
|
-
|
|
132
|
-
# If still too long, just truncate
|
|
133
|
-
if len(optimized_name) > available_space:
|
|
134
|
-
# Try to truncate at word boundary
|
|
135
|
-
truncated = optimized_name[:available_space]
|
|
136
|
-
last_underscore = truncated.rfind('_')
|
|
137
|
-
if last_underscore > available_space * 0.7: # Keep if we're not losing too much
|
|
138
|
-
optimized_name = truncated[:last_underscore]
|
|
139
|
-
else:
|
|
140
|
-
optimized_name = truncated
|
|
141
|
-
|
|
142
|
-
full_name = f'{prefix}{splitter}{optimized_name}'
|
|
143
|
-
logger.info(f"Optimized tool name: '{tool_name}' ({len(tool_name)} chars) -> '{optimized_name}' ({len(optimized_name)} chars), full: '{full_name}' ({len(full_name)} chars)")
|
|
144
|
-
|
|
145
|
-
return full_name
|
|
146
|
-
|
|
147
42
|
class McpToolkit(BaseToolkit):
|
|
148
43
|
"""
|
|
149
44
|
MCP Toolkit for connecting to a single remote MCP server and exposing its tools.
|
|
@@ -153,9 +48,6 @@ class McpToolkit(BaseToolkit):
|
|
|
153
48
|
tools: List[BaseTool] = []
|
|
154
49
|
toolkit_name: Optional[str] = None
|
|
155
50
|
|
|
156
|
-
# Class variable (not Pydantic field) for tool name length limit
|
|
157
|
-
toolkit_max_length: ClassVar[int] = 0 # No limit for MCP tool names
|
|
158
|
-
|
|
159
51
|
def __getstate__(self):
|
|
160
52
|
"""Custom serialization for pickle compatibility."""
|
|
161
53
|
state = self.__dict__.copy()
|
|
@@ -595,28 +487,23 @@ class McpToolkit(BaseToolkit):
|
|
|
595
487
|
) -> Optional[BaseTool]:
|
|
596
488
|
"""Create a BaseTool from a tool/prompt dictionary (from direct HTTP discovery)."""
|
|
597
489
|
try:
|
|
598
|
-
#
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
# Clean toolkit name for prefixing
|
|
602
|
-
clean_prefix = clean_string(toolkit_name, max_length_value)
|
|
603
|
-
|
|
604
|
-
# Optimize tool name to fit within 64 character limit
|
|
605
|
-
full_tool_name = optimize_tool_name(clean_prefix, tool_dict.get("name", "unknown"))
|
|
490
|
+
# Use original tool name directly
|
|
491
|
+
tool_name = tool_dict.get("name", "unknown")
|
|
606
492
|
|
|
607
493
|
# Check if this is a prompt (converted to tool)
|
|
608
494
|
is_prompt = tool_dict.get("_mcp_type") == "prompt"
|
|
609
495
|
item_type = "prompt" if is_prompt else "tool"
|
|
610
496
|
|
|
611
|
-
# Build description and ensure it doesn't exceed 1000 characters
|
|
612
|
-
|
|
497
|
+
# Build description with toolkit context and ensure it doesn't exceed 1000 characters
|
|
498
|
+
base_description = tool_dict.get('description', '')
|
|
499
|
+
description = f"{base_description}\nToolkit: {toolkit_name} ({connection_config.url})"
|
|
613
500
|
if len(description) > 1000:
|
|
614
501
|
description = description[:997] + "..."
|
|
615
|
-
logger.debug(f"Trimmed description for tool '{
|
|
502
|
+
logger.debug(f"Trimmed description for tool '{tool_name}' to 1000 chars")
|
|
616
503
|
|
|
617
504
|
# Use McpRemoteTool for remote MCP servers (HTTP/SSE)
|
|
618
505
|
return McpRemoteTool(
|
|
619
|
-
name=
|
|
506
|
+
name=tool_name,
|
|
620
507
|
description=description,
|
|
621
508
|
args_schema=McpServerTool.create_pydantic_model_from_schema(
|
|
622
509
|
tool_dict.get("inputSchema", {})
|
|
@@ -628,11 +515,11 @@ class McpToolkit(BaseToolkit):
|
|
|
628
515
|
tool_timeout_sec=timeout,
|
|
629
516
|
is_prompt=is_prompt,
|
|
630
517
|
prompt_name=tool_dict.get("_mcp_prompt_name") if is_prompt else None,
|
|
631
|
-
original_tool_name=
|
|
518
|
+
original_tool_name=tool_name, # Store original name for MCP server invocation
|
|
632
519
|
session_id=session_id # Pass session ID for stateful SSE servers
|
|
633
520
|
)
|
|
634
521
|
except Exception as e:
|
|
635
|
-
logger.error(f"Failed to create MCP tool '{
|
|
522
|
+
logger.error(f"Failed to create MCP tool '{tool_name}' from toolkit '{toolkit_name}': {e}")
|
|
636
523
|
return None
|
|
637
524
|
|
|
638
525
|
@classmethod
|
|
@@ -691,7 +578,7 @@ class McpToolkit(BaseToolkit):
|
|
|
691
578
|
# We don't have full connection config in static mode, so create a basic one
|
|
692
579
|
# The inspection tool will work as long as the server is accessible
|
|
693
580
|
inspection_tool = McpInspectTool(
|
|
694
|
-
name=
|
|
581
|
+
name="mcp_inspect",
|
|
695
582
|
server_name=toolkit_name,
|
|
696
583
|
server_url="", # Will be populated by the client if available
|
|
697
584
|
description=f"Inspect available tools, prompts, and resources from MCP toolkit '{toolkit_name}'"
|
|
@@ -713,22 +600,17 @@ class McpToolkit(BaseToolkit):
|
|
|
713
600
|
) -> Optional[BaseTool]:
|
|
714
601
|
"""Create a BaseTool from discovered metadata."""
|
|
715
602
|
try:
|
|
716
|
-
#
|
|
717
|
-
|
|
603
|
+
# Use original tool name directly
|
|
604
|
+
tool_name = tool_metadata.name
|
|
718
605
|
|
|
719
|
-
#
|
|
720
|
-
|
|
721
|
-
# Optimize tool name to fit within 64 character limit
|
|
722
|
-
full_tool_name = optimize_tool_name(clean_prefix, tool_metadata.name)
|
|
723
|
-
|
|
724
|
-
# Build description and ensure it doesn't exceed 1000 characters
|
|
725
|
-
description = f"MCP tool '{tool_metadata.name}' from server '{tool_metadata.server}': {tool_metadata.description}"
|
|
606
|
+
# Build description with toolkit context and ensure it doesn't exceed 1000 characters
|
|
607
|
+
description = f"{tool_metadata.description}\nToolkit: {toolkit_name}"
|
|
726
608
|
if len(description) > 1000:
|
|
727
609
|
description = description[:997] + "..."
|
|
728
|
-
logger.debug(f"Trimmed description for tool '{
|
|
610
|
+
logger.debug(f"Trimmed description for tool '{tool_name}' to 1000 chars")
|
|
729
611
|
|
|
730
612
|
return McpServerTool(
|
|
731
|
-
name=
|
|
613
|
+
name=tool_name,
|
|
732
614
|
description=description,
|
|
733
615
|
args_schema=McpServerTool.create_pydantic_model_from_schema(tool_metadata.input_schema),
|
|
734
616
|
client=client,
|
|
@@ -736,7 +618,7 @@ class McpToolkit(BaseToolkit):
|
|
|
736
618
|
tool_timeout_sec=timeout
|
|
737
619
|
)
|
|
738
620
|
except Exception as e:
|
|
739
|
-
logger.error(f"Failed to create MCP tool '{
|
|
621
|
+
logger.error(f"Failed to create MCP tool '{tool_name}' from server '{tool_metadata.server}': {e}")
|
|
740
622
|
return None
|
|
741
623
|
|
|
742
624
|
@classmethod
|
|
@@ -749,23 +631,18 @@ class McpToolkit(BaseToolkit):
|
|
|
749
631
|
) -> Optional[BaseTool]:
|
|
750
632
|
"""Create a single MCP tool."""
|
|
751
633
|
try:
|
|
752
|
-
#
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
# Clean toolkit name for prefixing
|
|
756
|
-
clean_prefix = clean_string(toolkit_name, max_length_value)
|
|
634
|
+
# Use original tool name directly
|
|
635
|
+
tool_name = available_tool["name"]
|
|
757
636
|
|
|
758
|
-
#
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
# Build description and ensure it doesn't exceed 1000 characters
|
|
762
|
-
description = f"MCP tool '{available_tool['name']}' from toolkit '{toolkit_name}': {available_tool.get('description', '')}"
|
|
637
|
+
# Build description with toolkit context and ensure it doesn't exceed 1000 characters
|
|
638
|
+
base_description = available_tool.get('description', '')
|
|
639
|
+
description = f"{base_description}\nToolkit: {toolkit_name}"
|
|
763
640
|
if len(description) > 1000:
|
|
764
641
|
description = description[:997] + "..."
|
|
765
|
-
logger.debug(f"Trimmed description for tool '{
|
|
642
|
+
logger.debug(f"Trimmed description for tool '{tool_name}' to 1000 chars")
|
|
766
643
|
|
|
767
644
|
return McpServerTool(
|
|
768
|
-
name=
|
|
645
|
+
name=tool_name,
|
|
769
646
|
description=description,
|
|
770
647
|
args_schema=McpServerTool.create_pydantic_model_from_schema(
|
|
771
648
|
available_tool.get("inputSchema", {})
|
|
@@ -775,7 +652,7 @@ class McpToolkit(BaseToolkit):
|
|
|
775
652
|
tool_timeout_sec=timeout
|
|
776
653
|
)
|
|
777
654
|
except Exception as e:
|
|
778
|
-
logger.error(f"Failed to create MCP tool '{
|
|
655
|
+
logger.error(f"Failed to create MCP tool '{tool_name}' from toolkit '{toolkit_name}': {e}")
|
|
779
656
|
return None
|
|
780
657
|
|
|
781
658
|
@classmethod
|
|
@@ -786,16 +663,8 @@ class McpToolkit(BaseToolkit):
|
|
|
786
663
|
) -> Optional[BaseTool]:
|
|
787
664
|
"""Create the inspection tool for the MCP toolkit."""
|
|
788
665
|
try:
|
|
789
|
-
# Store toolkit_max_length in local variable to avoid contextual access issues
|
|
790
|
-
max_length_value = cls.toolkit_max_length
|
|
791
|
-
|
|
792
|
-
# Clean toolkit name for prefixing
|
|
793
|
-
clean_prefix = clean_string(toolkit_name, max_length_value)
|
|
794
|
-
|
|
795
|
-
full_tool_name = f'{clean_prefix}{TOOLKIT_SPLITTER}mcp_inspect'
|
|
796
|
-
|
|
797
666
|
return McpInspectTool(
|
|
798
|
-
name=
|
|
667
|
+
name="mcp_inspect",
|
|
799
668
|
server_name=toolkit_name,
|
|
800
669
|
server_url=connection_config.url,
|
|
801
670
|
server_headers=connection_config.headers,
|
|
@@ -16,7 +16,7 @@ from pydantic.fields import FieldInfo
|
|
|
16
16
|
|
|
17
17
|
from ..tools.planning import PlanningWrapper
|
|
18
18
|
from ...tools.base.tool import BaseAction
|
|
19
|
-
from ...tools.utils import clean_string,
|
|
19
|
+
from ...tools.utils import clean_string, get_max_toolkit_length
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class PlanningToolkit(BaseToolkit):
|
|
@@ -150,8 +150,8 @@ class PlanningToolkit(BaseToolkit):
|
|
|
150
150
|
plan_callback=plan_callback,
|
|
151
151
|
)
|
|
152
152
|
|
|
153
|
-
#
|
|
154
|
-
|
|
153
|
+
# Use clean toolkit name for context (max 1000 chars in description)
|
|
154
|
+
toolkit_context = f" [Toolkit: {clean_string(toolkit_name, 0)}]" if toolkit_name else ''
|
|
155
155
|
|
|
156
156
|
# Create tools from wrapper
|
|
157
157
|
available_tools = wrapper.get_available_tools()
|
|
@@ -159,10 +159,15 @@ class PlanningToolkit(BaseToolkit):
|
|
|
159
159
|
if tool["name"] not in selected_tools:
|
|
160
160
|
continue
|
|
161
161
|
|
|
162
|
+
# Add toolkit context to description with character limit
|
|
163
|
+
description = tool["description"]
|
|
164
|
+
if toolkit_context and len(description + toolkit_context) <= 1000:
|
|
165
|
+
description = description + toolkit_context
|
|
166
|
+
|
|
162
167
|
tools.append(BaseAction(
|
|
163
168
|
api_wrapper=wrapper,
|
|
164
|
-
name=
|
|
165
|
-
description=
|
|
169
|
+
name=tool["name"],
|
|
170
|
+
description=description,
|
|
166
171
|
args_schema=tool["args_schema"]
|
|
167
172
|
))
|
|
168
173
|
|