alita-sdk 0.3.486__py3-none-any.whl → 0.3.515__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/agent_loader.py +27 -6
- alita_sdk/cli/agents.py +10 -1
- alita_sdk/cli/inventory.py +12 -195
- alita_sdk/cli/tools/filesystem.py +95 -9
- 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 +64 -40
- alita_sdk/runtime/clients/sandbox_client.py +14 -0
- alita_sdk/runtime/langchain/assistant.py +48 -2
- alita_sdk/runtime/langchain/constants.py +3 -1
- alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +103 -60
- 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 +12 -7
- alita_sdk/runtime/langchain/langraph_agent.py +10 -10
- 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 +94 -219
- alita_sdk/runtime/toolkits/planning.py +13 -6
- alita_sdk/runtime/toolkits/tools.py +60 -25
- alita_sdk/runtime/toolkits/vectorstore.py +11 -5
- alita_sdk/runtime/tools/artifact.py +185 -23
- alita_sdk/runtime/tools/function.py +2 -1
- alita_sdk/runtime/tools/llm.py +155 -34
- alita_sdk/runtime/tools/mcp_remote_tool.py +25 -10
- alita_sdk/runtime/tools/mcp_server_tool.py +2 -4
- alita_sdk/runtime/tools/vectorstore_base.py +3 -3
- alita_sdk/runtime/utils/AlitaCallback.py +136 -21
- alita_sdk/runtime/utils/mcp_client.py +492 -0
- alita_sdk/runtime/utils/mcp_oauth.py +125 -8
- alita_sdk/runtime/utils/mcp_sse_client.py +35 -6
- alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
- alita_sdk/runtime/utils/toolkit_utils.py +7 -13
- 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 +26 -1
- 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/chunkers/sematic/json_chunker.py +1 -0
- alita_sdk/tools/chunkers/sematic/markdown_chunker.py +2 -0
- alita_sdk/tools/chunkers/universal_chunker.py +1 -0
- 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/loaders/codesearcher.py +3 -2
- alita_sdk/tools/code/sonar/__init__.py +9 -7
- alita_sdk/tools/confluence/__init__.py +15 -10
- alita_sdk/tools/confluence/api_wrapper.py +63 -14
- 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/sharepoint/api_wrapper.py +2 -2
- 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/vector_adapters/VectorStoreAdapter.py +16 -18
- 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.486.dist-info → alita_sdk-0.3.515.dist-info}/METADATA +1 -1
- {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/RECORD +124 -119
- {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/entry_points.txt +0 -0
- {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/top_level.txt +0 -0
alita_sdk/cli/agent_loader.py
CHANGED
|
@@ -8,6 +8,7 @@ import json
|
|
|
8
8
|
import yaml
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Dict, Any
|
|
11
|
+
from pydantic import SecretStr
|
|
11
12
|
|
|
12
13
|
from .config import substitute_env_vars
|
|
13
14
|
|
|
@@ -85,6 +86,25 @@ def load_agent_definition(file_path: str) -> Dict[str, Any]:
|
|
|
85
86
|
raise ValueError(f"Unsupported file format: {path.suffix}")
|
|
86
87
|
|
|
87
88
|
|
|
89
|
+
def unwrap_secrets(obj: Any) -> Any:
|
|
90
|
+
"""
|
|
91
|
+
Recursively unwrap pydantic SecretStr values into plain strings.
|
|
92
|
+
|
|
93
|
+
Handles nested dicts, lists, tuples, and sets while preserving structure.
|
|
94
|
+
"""
|
|
95
|
+
if isinstance(obj, SecretStr):
|
|
96
|
+
return obj.get_secret_value()
|
|
97
|
+
if isinstance(obj, dict):
|
|
98
|
+
return {k: unwrap_secrets(v) for k, v in obj.items()}
|
|
99
|
+
if isinstance(obj, list):
|
|
100
|
+
return [unwrap_secrets(v) for v in obj]
|
|
101
|
+
if isinstance(obj, tuple):
|
|
102
|
+
return tuple(unwrap_secrets(v) for v in obj)
|
|
103
|
+
if isinstance(obj, set):
|
|
104
|
+
return {unwrap_secrets(v) for v in obj}
|
|
105
|
+
return obj
|
|
106
|
+
|
|
107
|
+
|
|
88
108
|
def build_agent_data_structure(agent_def: Dict[str, Any], toolkit_configs: list,
|
|
89
109
|
llm_model: str, llm_temperature: float, llm_max_tokens: int) -> Dict[str, Any]:
|
|
90
110
|
"""
|
|
@@ -128,12 +148,13 @@ def build_agent_data_structure(agent_def: Dict[str, Any], toolkit_configs: list,
|
|
|
128
148
|
if hasattr(toolkit_class, 'toolkit_config_schema'):
|
|
129
149
|
schema = toolkit_class.toolkit_config_schema()
|
|
130
150
|
validated_config = schema(**toolkit_config)
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
151
|
+
# Use python mode so SecretStr remains as objects, then unwrap recursively
|
|
152
|
+
validated_dict = unwrap_secrets(validated_config.model_dump(mode="python"))
|
|
153
|
+
validated_dict['type'] = toolkit_config.get('type')
|
|
154
|
+
validated_dict['toolkit_name'] = toolkit_config.get('toolkit_name')
|
|
155
|
+
validated_toolkit_configs.append(validated_dict)
|
|
156
|
+
else:
|
|
157
|
+
validated_toolkit_configs.append(toolkit_config)
|
|
137
158
|
else:
|
|
138
159
|
validated_toolkit_configs.append(toolkit_config)
|
|
139
160
|
except Exception:
|
alita_sdk/cli/agents.py
CHANGED
|
@@ -1358,12 +1358,14 @@ def agent_show(ctx, agent_source: str, version: Optional[str]):
|
|
|
1358
1358
|
help='Grant agent filesystem access to this directory')
|
|
1359
1359
|
@click.option('--verbose', '-v', type=click.Choice(['quiet', 'default', 'debug']), default='default',
|
|
1360
1360
|
help='Output verbosity level: quiet (final output only), default (tool calls + outputs), debug (all including LLM calls)')
|
|
1361
|
+
@click.option('--recursion-limit', type=int, default=50,
|
|
1362
|
+
help='Maximum number of tool execution steps per turn')
|
|
1361
1363
|
@click.pass_context
|
|
1362
1364
|
def agent_chat(ctx, agent_source: Optional[str], version: Optional[str],
|
|
1363
1365
|
toolkit_config: tuple, inventory_path: Optional[str], thread_id: Optional[str],
|
|
1364
1366
|
model: Optional[str], temperature: Optional[float],
|
|
1365
1367
|
max_tokens: Optional[int], work_dir: Optional[str],
|
|
1366
|
-
verbose: str):
|
|
1368
|
+
verbose: str, recursion_limit: Optional[int]):
|
|
1367
1369
|
"""Start interactive chat with an agent.
|
|
1368
1370
|
|
|
1369
1371
|
\b
|
|
@@ -2615,6 +2617,11 @@ def agent_chat(ctx, agent_source: Optional[str], version: Optional[str],
|
|
|
2615
2617
|
invoke_config = RunnableConfig(
|
|
2616
2618
|
configurable={"thread_id": current_session_id}
|
|
2617
2619
|
)
|
|
2620
|
+
# always proceed with continuation enabled
|
|
2621
|
+
invoke_config["should_continue"] = True
|
|
2622
|
+
# Set recursion limit for tool executions
|
|
2623
|
+
logger.debug(f"Setting tool steps limit to {recursion_limit}")
|
|
2624
|
+
invoke_config["recursion_limit"] = recursion_limit
|
|
2618
2625
|
cli_callback = None
|
|
2619
2626
|
if show_verbose:
|
|
2620
2627
|
cli_callback = create_cli_callback(verbose=True, debug=debug_mode)
|
|
@@ -2718,6 +2725,8 @@ def agent_chat(ctx, agent_source: Optional[str], version: Optional[str],
|
|
|
2718
2725
|
invoke_config = RunnableConfig(
|
|
2719
2726
|
configurable={"thread_id": continuation_thread_id}
|
|
2720
2727
|
)
|
|
2728
|
+
invoke_config["should_continue"] = True
|
|
2729
|
+
invoke_config["recursion_limit"] = recursion_limit
|
|
2721
2730
|
if cli_callback:
|
|
2722
2731
|
invoke_config["callbacks"] = [cli_callback]
|
|
2723
2732
|
|
alita_sdk/cli/inventory.py
CHANGED
|
@@ -1048,209 +1048,26 @@ def _get_checkpoint_path(graph: str, source_name: str) -> str:
|
|
|
1048
1048
|
|
|
1049
1049
|
|
|
1050
1050
|
def _load_toolkit_config(toolkit_path: str) -> Dict[str, Any]:
|
|
1051
|
-
"""
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
Supports environment variable substitution for values like ${GITHUB_PAT}.
|
|
1055
|
-
"""
|
|
1056
|
-
with open(toolkit_path, 'r') as f:
|
|
1057
|
-
config = json.load(f)
|
|
1058
|
-
|
|
1059
|
-
# Recursively resolve environment variables
|
|
1060
|
-
def resolve_env_vars(obj):
|
|
1061
|
-
if isinstance(obj, str):
|
|
1062
|
-
# Match ${VAR_NAME} pattern
|
|
1063
|
-
pattern = r'\$\{([^}]+)\}'
|
|
1064
|
-
matches = re.findall(pattern, obj)
|
|
1065
|
-
for var_name in matches:
|
|
1066
|
-
env_value = os.environ.get(var_name, '')
|
|
1067
|
-
obj = obj.replace(f'${{{var_name}}}', env_value)
|
|
1068
|
-
return obj
|
|
1069
|
-
elif isinstance(obj, dict):
|
|
1070
|
-
return {k: resolve_env_vars(v) for k, v in obj.items()}
|
|
1071
|
-
elif isinstance(obj, list):
|
|
1072
|
-
return [resolve_env_vars(item) for item in obj]
|
|
1073
|
-
return obj
|
|
1074
|
-
|
|
1075
|
-
return resolve_env_vars(config)
|
|
1051
|
+
"""Deprecated: Use alita_sdk.community.inventory.toolkit_utils.load_toolkit_config instead."""
|
|
1052
|
+
from alita_sdk.community.inventory.toolkit_utils import load_toolkit_config
|
|
1053
|
+
return load_toolkit_config(toolkit_path)
|
|
1076
1054
|
|
|
1077
1055
|
|
|
1078
1056
|
def _get_llm(ctx, model: Optional[str] = None, temperature: float = 0.0):
|
|
1079
|
-
"""
|
|
1057
|
+
"""Deprecated: Use alita_sdk.community.inventory.toolkit_utils.get_llm_for_config instead."""
|
|
1080
1058
|
from .cli import get_client
|
|
1059
|
+
from alita_sdk.community.inventory.toolkit_utils import get_llm_for_config
|
|
1081
1060
|
|
|
1082
|
-
# Get Alita client - this will raise ClickException if not configured
|
|
1083
1061
|
client = get_client(ctx)
|
|
1084
|
-
|
|
1085
|
-
# Get model name from parameter or default
|
|
1086
|
-
model_name = model or 'gpt-4o-mini'
|
|
1087
|
-
|
|
1088
|
-
# Use client.get_llm() which is the actual method
|
|
1089
|
-
return client.get_llm(
|
|
1090
|
-
model_name=model_name,
|
|
1091
|
-
model_config={
|
|
1092
|
-
'temperature': temperature,
|
|
1093
|
-
'max_tokens': 4096
|
|
1094
|
-
}
|
|
1095
|
-
)
|
|
1062
|
+
return get_llm_for_config(client, model=model, temperature=temperature)
|
|
1096
1063
|
|
|
1097
1064
|
|
|
1098
1065
|
def _get_source_toolkit(toolkit_config: Dict[str, Any]):
|
|
1099
|
-
"""
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
Uses the SDK's toolkit factory pattern - all toolkits extend BaseCodeToolApiWrapper
|
|
1103
|
-
or BaseVectorStoreToolApiWrapper, which have loader() and chunker() methods.
|
|
1104
|
-
|
|
1105
|
-
Also supports CLI-specific toolkits like 'filesystem' for local document loading.
|
|
1066
|
+
"""Deprecated: Use alita_sdk.community.inventory.toolkit_utils.get_source_toolkit instead."""
|
|
1067
|
+
from alita_sdk.community.inventory.toolkit_utils import get_source_toolkit
|
|
1106
1068
|
|
|
1107
|
-
Args:
|
|
1108
|
-
toolkit_config: Toolkit configuration dict with 'type' and settings
|
|
1109
|
-
|
|
1110
|
-
Returns:
|
|
1111
|
-
API wrapper instance with loader() method
|
|
1112
|
-
"""
|
|
1113
|
-
source = toolkit_config.get('type')
|
|
1114
|
-
if not source:
|
|
1115
|
-
raise click.ClickException("Toolkit config missing 'type' field")
|
|
1116
|
-
|
|
1117
|
-
# Handle filesystem type (CLI-specific, not in AVAILABLE_TOOLS)
|
|
1118
|
-
if source == 'filesystem':
|
|
1119
|
-
from .tools.filesystem import FilesystemApiWrapper
|
|
1120
|
-
|
|
1121
|
-
base_directory = (
|
|
1122
|
-
toolkit_config.get('base_directory') or
|
|
1123
|
-
toolkit_config.get('path') or
|
|
1124
|
-
toolkit_config.get('git_root_dir')
|
|
1125
|
-
)
|
|
1126
|
-
|
|
1127
|
-
if not base_directory:
|
|
1128
|
-
raise click.ClickException(
|
|
1129
|
-
"Filesystem toolkit requires 'base_directory' or 'path' field"
|
|
1130
|
-
)
|
|
1131
|
-
|
|
1132
|
-
return FilesystemApiWrapper(
|
|
1133
|
-
base_directory=base_directory,
|
|
1134
|
-
recursive=toolkit_config.get('recursive', True),
|
|
1135
|
-
follow_symlinks=toolkit_config.get('follow_symlinks', False),
|
|
1136
|
-
)
|
|
1137
|
-
|
|
1138
|
-
# Handle standard SDK toolkits via AVAILABLE_TOOLS registry
|
|
1139
|
-
from alita_sdk.tools import AVAILABLE_TOOLS
|
|
1140
|
-
|
|
1141
|
-
# Check if toolkit type is available
|
|
1142
|
-
if source not in AVAILABLE_TOOLS:
|
|
1143
|
-
raise click.ClickException(
|
|
1144
|
-
f"Unknown toolkit type: {source}. "
|
|
1145
|
-
f"Available: {', '.join(list(AVAILABLE_TOOLS.keys()) + ['filesystem'])}"
|
|
1146
|
-
)
|
|
1147
|
-
|
|
1148
|
-
toolkit_info = AVAILABLE_TOOLS[source]
|
|
1149
|
-
|
|
1150
|
-
# Get the toolkit class
|
|
1151
|
-
if 'toolkit_class' not in toolkit_info:
|
|
1152
|
-
raise click.ClickException(
|
|
1153
|
-
f"Toolkit '{source}' does not have a toolkit_class registered"
|
|
1154
|
-
)
|
|
1155
|
-
|
|
1156
|
-
toolkit_class = toolkit_info['toolkit_class']
|
|
1157
|
-
|
|
1158
|
-
# Build kwargs from toolkit config - we need to map config to API wrapper params
|
|
1159
|
-
kwargs = dict(toolkit_config)
|
|
1160
|
-
|
|
1161
|
-
# Remove fields that aren't needed for the API wrapper
|
|
1162
|
-
kwargs.pop('type', None)
|
|
1163
|
-
kwargs.pop('toolkit_name', None)
|
|
1164
|
-
kwargs.pop('selected_tools', None)
|
|
1165
|
-
kwargs.pop('excluded_tools', None)
|
|
1166
|
-
|
|
1167
|
-
# Handle common config patterns - flatten nested configurations
|
|
1168
|
-
config_key = f"{source}_configuration"
|
|
1169
|
-
if config_key in kwargs:
|
|
1170
|
-
nested_config = kwargs.pop(config_key)
|
|
1171
|
-
if isinstance(nested_config, dict):
|
|
1172
|
-
kwargs.update(nested_config)
|
|
1173
|
-
|
|
1174
|
-
# Handle ADO-specific config pattern
|
|
1175
|
-
if 'ado_configuration' in kwargs:
|
|
1176
|
-
ado_config = kwargs.pop('ado_configuration')
|
|
1177
|
-
if isinstance(ado_config, dict):
|
|
1178
|
-
kwargs.update(ado_config)
|
|
1179
|
-
|
|
1180
|
-
# Expand environment variables in string values (e.g., ${GITHUB_PAT})
|
|
1181
|
-
def expand_env_vars(value):
|
|
1182
|
-
"""Recursively expand environment variables in values."""
|
|
1183
|
-
if isinstance(value, str):
|
|
1184
|
-
import re
|
|
1185
|
-
# Match ${VAR} or $VAR patterns
|
|
1186
|
-
pattern = r'\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)'
|
|
1187
|
-
def replace(match):
|
|
1188
|
-
var_name = match.group(1) or match.group(2)
|
|
1189
|
-
return os.environ.get(var_name, match.group(0))
|
|
1190
|
-
return re.sub(pattern, replace, value)
|
|
1191
|
-
elif isinstance(value, dict):
|
|
1192
|
-
return {k: expand_env_vars(v) for k, v in value.items()}
|
|
1193
|
-
elif isinstance(value, list):
|
|
1194
|
-
return [expand_env_vars(v) for v in value]
|
|
1195
|
-
return value
|
|
1196
|
-
|
|
1197
|
-
kwargs = expand_env_vars(kwargs)
|
|
1198
|
-
|
|
1199
|
-
# Map common field names to API wrapper expected names
|
|
1200
|
-
# GitHub: personal_access_token -> github_access_token
|
|
1201
|
-
if 'personal_access_token' in kwargs and source == 'github':
|
|
1202
|
-
kwargs['github_access_token'] = kwargs.pop('personal_access_token')
|
|
1203
|
-
|
|
1204
|
-
# GitHub: repository -> github_repository
|
|
1205
|
-
if 'repository' in kwargs and source == 'github':
|
|
1206
|
-
kwargs['github_repository'] = kwargs.pop('repository')
|
|
1207
|
-
|
|
1208
|
-
# Ensure active_branch has a default
|
|
1209
|
-
if 'active_branch' not in kwargs:
|
|
1210
|
-
kwargs['active_branch'] = kwargs.get('base_branch', 'main')
|
|
1211
|
-
|
|
1212
|
-
# Get the API wrapper class from the toolkit
|
|
1213
|
-
# Introspect toolkit to find the API wrapper class it uses
|
|
1214
1069
|
try:
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
wrapper_module = importlib.import_module(module_path)
|
|
1220
|
-
except ImportError:
|
|
1221
|
-
# Try alternate path for nested modules
|
|
1222
|
-
module_path = f"alita_sdk.tools.{source.replace('_', '.')}.api_wrapper"
|
|
1223
|
-
wrapper_module = importlib.import_module(module_path)
|
|
1224
|
-
|
|
1225
|
-
# Find the API wrapper class - look for class containing ApiWrapper/APIWrapper
|
|
1226
|
-
api_wrapper_class = None
|
|
1227
|
-
for name in dir(wrapper_module):
|
|
1228
|
-
obj = getattr(wrapper_module, name)
|
|
1229
|
-
if (isinstance(obj, type) and
|
|
1230
|
-
('ApiWrapper' in name or 'APIWrapper' in name) and
|
|
1231
|
-
name not in ('BaseCodeToolApiWrapper', 'BaseVectorStoreToolApiWrapper', 'BaseToolApiWrapper')):
|
|
1232
|
-
api_wrapper_class = obj
|
|
1233
|
-
break
|
|
1234
|
-
|
|
1235
|
-
if not api_wrapper_class:
|
|
1236
|
-
raise click.ClickException(
|
|
1237
|
-
f"Could not find API wrapper class in {module_path}"
|
|
1238
|
-
)
|
|
1239
|
-
|
|
1240
|
-
# Instantiate the API wrapper directly
|
|
1241
|
-
api_wrapper = api_wrapper_class(**kwargs)
|
|
1242
|
-
|
|
1243
|
-
# Verify it has loader method
|
|
1244
|
-
if not hasattr(api_wrapper, 'loader'):
|
|
1245
|
-
raise click.ClickException(
|
|
1246
|
-
f"API wrapper '{api_wrapper_class.__name__}' has no loader() method"
|
|
1247
|
-
)
|
|
1248
|
-
|
|
1249
|
-
return api_wrapper
|
|
1250
|
-
|
|
1251
|
-
except ImportError as e:
|
|
1252
|
-
logger.exception(f"Failed to import API wrapper for {source}")
|
|
1253
|
-
raise click.ClickException(f"Failed to import {source} API wrapper: {e}")
|
|
1254
|
-
except Exception as e:
|
|
1255
|
-
logger.exception(f"Failed to instantiate toolkit {source}")
|
|
1256
|
-
raise click.ClickException(f"Failed to create {source} toolkit: {e}")
|
|
1070
|
+
return get_source_toolkit(toolkit_config)
|
|
1071
|
+
except ValueError as e:
|
|
1072
|
+
# Convert ValueError to ClickException for CLI context
|
|
1073
|
+
raise click.ClickException(str(e))
|
|
@@ -135,6 +135,7 @@ class ListDirectoryInput(BaseModel):
|
|
|
135
135
|
path: str = Field(default=".", description="Relative path to the directory to list")
|
|
136
136
|
include_sizes: bool = Field(default=False, description="Include file sizes in the output")
|
|
137
137
|
sort_by: str = Field(default="name", description="Sort by 'name' or 'size'")
|
|
138
|
+
max_results: Optional[int] = Field(default=200, description="Maximum number of entries to return. Default is 200 to prevent context overflow.")
|
|
138
139
|
|
|
139
140
|
|
|
140
141
|
class DirectoryTreeInput(BaseModel):
|
|
@@ -181,6 +182,8 @@ class FileSystemTool(BaseTool):
|
|
|
181
182
|
"""Base class for filesystem tools with directory restriction."""
|
|
182
183
|
base_directory: str # Primary directory (for backward compatibility)
|
|
183
184
|
allowed_directories: List[str] = [] # Additional allowed directories
|
|
185
|
+
_basename_collision_detected: bool = False # Cache for collision detection
|
|
186
|
+
_basename_collision_checked: bool = False # Whether we've checked for collisions
|
|
184
187
|
|
|
185
188
|
def _get_all_allowed_directories(self) -> List[Path]:
|
|
186
189
|
"""Get all allowed directories as resolved Paths."""
|
|
@@ -191,6 +194,56 @@ class FileSystemTool(BaseTool):
|
|
|
191
194
|
dirs.append(resolved)
|
|
192
195
|
return dirs
|
|
193
196
|
|
|
197
|
+
def _check_basename_collision(self) -> bool:
|
|
198
|
+
"""Check if multiple allowed directories have the same basename."""
|
|
199
|
+
if self._basename_collision_checked:
|
|
200
|
+
return self._basename_collision_detected
|
|
201
|
+
|
|
202
|
+
allowed_dirs = self._get_all_allowed_directories()
|
|
203
|
+
basenames = [d.name for d in allowed_dirs]
|
|
204
|
+
self._basename_collision_detected = len(basenames) != len(set(basenames))
|
|
205
|
+
self._basename_collision_checked = True
|
|
206
|
+
return self._basename_collision_detected
|
|
207
|
+
|
|
208
|
+
def _get_relative_path_from_allowed_dirs(self, absolute_path: Path) -> tuple:
|
|
209
|
+
"""Get relative path and directory name for a file in allowed directories.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
absolute_path: Absolute path to the file
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Tuple of (relative_path, directory_name)
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
ValueError: If path is not within any allowed directory
|
|
219
|
+
"""
|
|
220
|
+
allowed_dirs = self._get_all_allowed_directories()
|
|
221
|
+
|
|
222
|
+
# Find which allowed directory contains this path
|
|
223
|
+
for base in allowed_dirs:
|
|
224
|
+
try:
|
|
225
|
+
rel_path = absolute_path.relative_to(base)
|
|
226
|
+
|
|
227
|
+
# Determine directory name for prefix
|
|
228
|
+
if self._check_basename_collision():
|
|
229
|
+
# Use parent/basename format to disambiguate
|
|
230
|
+
dir_name = f"{base.parent.name}/{base.name}"
|
|
231
|
+
else:
|
|
232
|
+
# Use just basename
|
|
233
|
+
dir_name = base.name
|
|
234
|
+
|
|
235
|
+
return (str(rel_path), dir_name)
|
|
236
|
+
except ValueError:
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
# Path not in any allowed directory
|
|
240
|
+
allowed_paths = [str(d) for d in allowed_dirs]
|
|
241
|
+
raise ValueError(
|
|
242
|
+
f"Path '{absolute_path}' is not within any allowed directory.\n"
|
|
243
|
+
f"Allowed directories: {allowed_paths}\n"
|
|
244
|
+
f"Attempted path: {absolute_path}"
|
|
245
|
+
)
|
|
246
|
+
|
|
194
247
|
def _resolve_path(self, relative_path: str) -> Path:
|
|
195
248
|
"""
|
|
196
249
|
Resolve and validate a path within any of the allowed directories.
|
|
@@ -602,7 +655,7 @@ class ListDirectoryTool(FileSystemTool):
|
|
|
602
655
|
"Consider using filesystem_directory_tree with max_depth=1 for hierarchical overview",
|
|
603
656
|
]
|
|
604
657
|
|
|
605
|
-
def _run(self, path: str = ".", include_sizes: bool = False, sort_by: str = "name") -> str:
|
|
658
|
+
def _run(self, path: str = ".", include_sizes: bool = False, sort_by: str = "name", max_results: Optional[int] = 200) -> str:
|
|
606
659
|
"""List directory contents."""
|
|
607
660
|
try:
|
|
608
661
|
target = self._resolve_path(path)
|
|
@@ -618,7 +671,8 @@ class ListDirectoryTool(FileSystemTool):
|
|
|
618
671
|
entry_info = {
|
|
619
672
|
'name': entry.name,
|
|
620
673
|
'is_dir': entry.is_dir(),
|
|
621
|
-
'size': entry.stat().st_size if entry.is_file() else 0
|
|
674
|
+
'size': entry.stat().st_size if entry.is_file() else 0,
|
|
675
|
+
'path': entry
|
|
622
676
|
}
|
|
623
677
|
entries.append(entry_info)
|
|
624
678
|
|
|
@@ -628,6 +682,18 @@ class ListDirectoryTool(FileSystemTool):
|
|
|
628
682
|
else:
|
|
629
683
|
entries.sort(key=lambda x: x['name'].lower())
|
|
630
684
|
|
|
685
|
+
# Apply limit
|
|
686
|
+
total_count = len(entries)
|
|
687
|
+
truncated = False
|
|
688
|
+
if max_results is not None and total_count > max_results:
|
|
689
|
+
entries = entries[:max_results]
|
|
690
|
+
truncated = True
|
|
691
|
+
|
|
692
|
+
# Get directory name for multi-directory configs
|
|
693
|
+
allowed_dirs = self._get_all_allowed_directories()
|
|
694
|
+
has_multiple_dirs = len(allowed_dirs) > 1
|
|
695
|
+
_, dir_name = self._get_relative_path_from_allowed_dirs(target) if has_multiple_dirs else ("", "")
|
|
696
|
+
|
|
631
697
|
# Format output
|
|
632
698
|
lines = []
|
|
633
699
|
total_files = 0
|
|
@@ -636,7 +702,12 @@ class ListDirectoryTool(FileSystemTool):
|
|
|
636
702
|
|
|
637
703
|
for entry in entries:
|
|
638
704
|
prefix = "[DIR] " if entry['is_dir'] else "[FILE]"
|
|
639
|
-
|
|
705
|
+
|
|
706
|
+
# Add directory prefix for multi-directory configs
|
|
707
|
+
if has_multiple_dirs:
|
|
708
|
+
name = f"{dir_name}/{entry['name']}"
|
|
709
|
+
else:
|
|
710
|
+
name = entry['name']
|
|
640
711
|
|
|
641
712
|
if include_sizes and not entry['is_dir']:
|
|
642
713
|
size_str = self._format_size(entry['size'])
|
|
@@ -665,6 +736,10 @@ class ListDirectoryTool(FileSystemTool):
|
|
|
665
736
|
summary += f"\nCombined size: {self._format_size(total_size)}"
|
|
666
737
|
result += summary
|
|
667
738
|
|
|
739
|
+
if truncated:
|
|
740
|
+
result += f"\n\n⚠️ OUTPUT TRUNCATED: Showing {len(entries)} of {total_count} entries from '{dir_name if has_multiple_dirs else path}' (max_results={max_results})"
|
|
741
|
+
result += "\n To see more: increase max_results or list a specific subdirectory"
|
|
742
|
+
|
|
668
743
|
# Add note about how to access files
|
|
669
744
|
result += "\n\nNote: Access files using paths shown above (e.g., 'agents/file.md' for items in agents/ directory)"
|
|
670
745
|
|
|
@@ -818,23 +893,34 @@ class SearchFilesTool(FileSystemTool):
|
|
|
818
893
|
else:
|
|
819
894
|
matches = sorted(all_matches)
|
|
820
895
|
|
|
821
|
-
# Format results
|
|
822
|
-
|
|
896
|
+
# Format results with directory prefixes for multi-directory configs
|
|
897
|
+
allowed_dirs = self._get_all_allowed_directories()
|
|
898
|
+
has_multiple_dirs = len(allowed_dirs) > 1
|
|
823
899
|
results = []
|
|
900
|
+
search_dir_name = None
|
|
824
901
|
|
|
825
902
|
for match in matches:
|
|
826
|
-
|
|
903
|
+
if has_multiple_dirs:
|
|
904
|
+
rel_path_str, dir_name = self._get_relative_path_from_allowed_dirs(match)
|
|
905
|
+
display_path = f"{dir_name}/{rel_path_str}"
|
|
906
|
+
if search_dir_name is None:
|
|
907
|
+
search_dir_name = dir_name
|
|
908
|
+
else:
|
|
909
|
+
rel_path_str = str(match.relative_to(Path(self.base_directory).resolve()))
|
|
910
|
+
display_path = rel_path_str
|
|
911
|
+
|
|
827
912
|
if match.is_dir():
|
|
828
|
-
results.append(f"📁 {
|
|
913
|
+
results.append(f"📁 {display_path}/")
|
|
829
914
|
else:
|
|
830
915
|
size = self._format_size(match.stat().st_size)
|
|
831
|
-
results.append(f"📄 {
|
|
916
|
+
results.append(f"📄 {display_path} ({size})")
|
|
832
917
|
|
|
833
918
|
header = f"Found {total_count} matches for '{pattern}':\n\n"
|
|
834
919
|
output = header + "\n".join(results)
|
|
835
920
|
|
|
836
921
|
if truncated:
|
|
837
|
-
|
|
922
|
+
location_str = f"from '{search_dir_name}' " if search_dir_name else ""
|
|
923
|
+
output += f"\n\n⚠️ OUTPUT TRUNCATED: Showing {max_results} of {total_count} results {location_str}(max_results={max_results})"
|
|
838
924
|
output += "\n To see more: increase max_results or use a more specific pattern"
|
|
839
925
|
|
|
840
926
|
return output
|
|
@@ -85,6 +85,13 @@ from .ingestion import (
|
|
|
85
85
|
# Retrieval Toolkit - for querying graphs
|
|
86
86
|
from .retrieval import InventoryRetrievalApiWrapper
|
|
87
87
|
|
|
88
|
+
# Toolkit utilities - for configuration and instantiation
|
|
89
|
+
from .toolkit_utils import (
|
|
90
|
+
load_toolkit_config,
|
|
91
|
+
get_llm_for_config,
|
|
92
|
+
get_source_toolkit,
|
|
93
|
+
)
|
|
94
|
+
|
|
88
95
|
# Core graph types
|
|
89
96
|
from .knowledge_graph import KnowledgeGraph, Citation
|
|
90
97
|
|
|
@@ -187,6 +194,11 @@ __all__ = [
|
|
|
187
194
|
'InventoryRetrievalToolkit',
|
|
188
195
|
'InventoryRetrievalApiWrapper',
|
|
189
196
|
|
|
197
|
+
# Toolkit utilities
|
|
198
|
+
'load_toolkit_config',
|
|
199
|
+
'get_llm_for_config',
|
|
200
|
+
'get_source_toolkit',
|
|
201
|
+
|
|
190
202
|
# Core types
|
|
191
203
|
'KnowledgeGraph',
|
|
192
204
|
'Citation',
|
|
@@ -15,7 +15,7 @@ from pydantic import BaseModel, Field, ConfigDict, create_model
|
|
|
15
15
|
|
|
16
16
|
from .retrieval import InventoryRetrievalApiWrapper
|
|
17
17
|
from ...tools.base.tool import BaseAction
|
|
18
|
-
from ...tools.utils import clean_string,
|
|
18
|
+
from ...tools.utils import clean_string, get_max_toolkit_length
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class InventoryRetrievalToolkit(BaseToolkit):
|
|
@@ -144,17 +144,21 @@ class InventoryRetrievalToolkit(BaseToolkit):
|
|
|
144
144
|
# Build tool mapping
|
|
145
145
|
tool_map = {t['name']: t for t in available_tools}
|
|
146
146
|
|
|
147
|
-
#
|
|
148
|
-
|
|
147
|
+
# Use clean toolkit name for context (max 1000 chars in description)
|
|
148
|
+
toolkit_context = f" [Toolkit: {clean_string(toolkit_name, 0)}]" if toolkit_name else ''
|
|
149
149
|
|
|
150
150
|
tools = []
|
|
151
151
|
for tool_name in selected_tools:
|
|
152
152
|
if tool_name in tool_map:
|
|
153
153
|
tool_info = tool_map[tool_name]
|
|
154
|
+
# Add toolkit context to description with character limit
|
|
155
|
+
description = tool_info['description']
|
|
156
|
+
if toolkit_context and len(description + toolkit_context) <= 1000:
|
|
157
|
+
description = description + toolkit_context
|
|
154
158
|
tools.append(BaseAction(
|
|
155
159
|
api_wrapper=api_wrapper,
|
|
156
|
-
name=
|
|
157
|
-
description=
|
|
160
|
+
name=tool_name,
|
|
161
|
+
description=description,
|
|
158
162
|
args_schema=tool_info['args_schema']
|
|
159
163
|
))
|
|
160
164
|
|