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.

Files changed (124) hide show
  1. alita_sdk/cli/agent_loader.py +27 -6
  2. alita_sdk/cli/agents.py +10 -1
  3. alita_sdk/cli/inventory.py +12 -195
  4. alita_sdk/cli/tools/filesystem.py +95 -9
  5. alita_sdk/community/inventory/__init__.py +12 -0
  6. alita_sdk/community/inventory/toolkit.py +9 -5
  7. alita_sdk/community/inventory/toolkit_utils.py +176 -0
  8. alita_sdk/configurations/ado.py +144 -0
  9. alita_sdk/configurations/confluence.py +76 -42
  10. alita_sdk/configurations/figma.py +76 -0
  11. alita_sdk/configurations/gitlab.py +2 -0
  12. alita_sdk/configurations/qtest.py +72 -1
  13. alita_sdk/configurations/report_portal.py +96 -0
  14. alita_sdk/configurations/sharepoint.py +148 -0
  15. alita_sdk/configurations/testio.py +83 -0
  16. alita_sdk/runtime/clients/artifact.py +2 -2
  17. alita_sdk/runtime/clients/client.py +64 -40
  18. alita_sdk/runtime/clients/sandbox_client.py +14 -0
  19. alita_sdk/runtime/langchain/assistant.py +48 -2
  20. alita_sdk/runtime/langchain/constants.py +3 -1
  21. alita_sdk/runtime/langchain/document_loaders/AlitaExcelLoader.py +103 -60
  22. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLinesLoader.py +77 -0
  23. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +2 -1
  24. alita_sdk/runtime/langchain/document_loaders/constants.py +12 -7
  25. alita_sdk/runtime/langchain/langraph_agent.py +10 -10
  26. alita_sdk/runtime/langchain/utils.py +6 -1
  27. alita_sdk/runtime/toolkits/artifact.py +14 -5
  28. alita_sdk/runtime/toolkits/datasource.py +13 -6
  29. alita_sdk/runtime/toolkits/mcp.py +94 -219
  30. alita_sdk/runtime/toolkits/planning.py +13 -6
  31. alita_sdk/runtime/toolkits/tools.py +60 -25
  32. alita_sdk/runtime/toolkits/vectorstore.py +11 -5
  33. alita_sdk/runtime/tools/artifact.py +185 -23
  34. alita_sdk/runtime/tools/function.py +2 -1
  35. alita_sdk/runtime/tools/llm.py +155 -34
  36. alita_sdk/runtime/tools/mcp_remote_tool.py +25 -10
  37. alita_sdk/runtime/tools/mcp_server_tool.py +2 -4
  38. alita_sdk/runtime/tools/vectorstore_base.py +3 -3
  39. alita_sdk/runtime/utils/AlitaCallback.py +136 -21
  40. alita_sdk/runtime/utils/mcp_client.py +492 -0
  41. alita_sdk/runtime/utils/mcp_oauth.py +125 -8
  42. alita_sdk/runtime/utils/mcp_sse_client.py +35 -6
  43. alita_sdk/runtime/utils/mcp_tools_discovery.py +124 -0
  44. alita_sdk/runtime/utils/toolkit_utils.py +7 -13
  45. alita_sdk/runtime/utils/utils.py +2 -0
  46. alita_sdk/tools/__init__.py +15 -0
  47. alita_sdk/tools/ado/repos/__init__.py +10 -12
  48. alita_sdk/tools/ado/test_plan/__init__.py +23 -8
  49. alita_sdk/tools/ado/wiki/__init__.py +24 -8
  50. alita_sdk/tools/ado/wiki/ado_wrapper.py +21 -7
  51. alita_sdk/tools/ado/work_item/__init__.py +24 -8
  52. alita_sdk/tools/advanced_jira_mining/__init__.py +10 -8
  53. alita_sdk/tools/aws/delta_lake/__init__.py +12 -9
  54. alita_sdk/tools/aws/delta_lake/tool.py +5 -1
  55. alita_sdk/tools/azure_ai/search/__init__.py +9 -7
  56. alita_sdk/tools/base/tool.py +5 -1
  57. alita_sdk/tools/base_indexer_toolkit.py +26 -1
  58. alita_sdk/tools/bitbucket/__init__.py +14 -10
  59. alita_sdk/tools/bitbucket/api_wrapper.py +50 -2
  60. alita_sdk/tools/browser/__init__.py +5 -4
  61. alita_sdk/tools/carrier/__init__.py +5 -6
  62. alita_sdk/tools/chunkers/sematic/json_chunker.py +1 -0
  63. alita_sdk/tools/chunkers/sematic/markdown_chunker.py +2 -0
  64. alita_sdk/tools/chunkers/universal_chunker.py +1 -0
  65. alita_sdk/tools/cloud/aws/__init__.py +9 -7
  66. alita_sdk/tools/cloud/azure/__init__.py +9 -7
  67. alita_sdk/tools/cloud/gcp/__init__.py +9 -7
  68. alita_sdk/tools/cloud/k8s/__init__.py +9 -7
  69. alita_sdk/tools/code/linter/__init__.py +9 -8
  70. alita_sdk/tools/code/loaders/codesearcher.py +3 -2
  71. alita_sdk/tools/code/sonar/__init__.py +9 -7
  72. alita_sdk/tools/confluence/__init__.py +15 -10
  73. alita_sdk/tools/confluence/api_wrapper.py +63 -14
  74. alita_sdk/tools/custom_open_api/__init__.py +11 -5
  75. alita_sdk/tools/elastic/__init__.py +10 -8
  76. alita_sdk/tools/elitea_base.py +387 -9
  77. alita_sdk/tools/figma/__init__.py +8 -7
  78. alita_sdk/tools/github/__init__.py +12 -14
  79. alita_sdk/tools/github/github_client.py +68 -2
  80. alita_sdk/tools/github/tool.py +5 -1
  81. alita_sdk/tools/gitlab/__init__.py +14 -11
  82. alita_sdk/tools/gitlab/api_wrapper.py +81 -1
  83. alita_sdk/tools/gitlab_org/__init__.py +9 -8
  84. alita_sdk/tools/google/bigquery/__init__.py +12 -12
  85. alita_sdk/tools/google/bigquery/tool.py +5 -1
  86. alita_sdk/tools/google_places/__init__.py +9 -8
  87. alita_sdk/tools/jira/__init__.py +15 -10
  88. alita_sdk/tools/keycloak/__init__.py +10 -8
  89. alita_sdk/tools/localgit/__init__.py +8 -3
  90. alita_sdk/tools/localgit/local_git.py +62 -54
  91. alita_sdk/tools/localgit/tool.py +5 -1
  92. alita_sdk/tools/memory/__init__.py +11 -3
  93. alita_sdk/tools/ocr/__init__.py +10 -8
  94. alita_sdk/tools/openapi/__init__.py +6 -2
  95. alita_sdk/tools/pandas/__init__.py +9 -7
  96. alita_sdk/tools/postman/__init__.py +10 -11
  97. alita_sdk/tools/pptx/__init__.py +9 -9
  98. alita_sdk/tools/qtest/__init__.py +9 -8
  99. alita_sdk/tools/rally/__init__.py +9 -8
  100. alita_sdk/tools/report_portal/__init__.py +11 -9
  101. alita_sdk/tools/salesforce/__init__.py +9 -9
  102. alita_sdk/tools/servicenow/__init__.py +10 -8
  103. alita_sdk/tools/sharepoint/__init__.py +9 -8
  104. alita_sdk/tools/sharepoint/api_wrapper.py +2 -2
  105. alita_sdk/tools/slack/__init__.py +8 -7
  106. alita_sdk/tools/sql/__init__.py +9 -8
  107. alita_sdk/tools/testio/__init__.py +9 -8
  108. alita_sdk/tools/testrail/__init__.py +10 -8
  109. alita_sdk/tools/utils/__init__.py +9 -4
  110. alita_sdk/tools/utils/text_operations.py +254 -0
  111. alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +16 -18
  112. alita_sdk/tools/xray/__init__.py +10 -8
  113. alita_sdk/tools/yagmail/__init__.py +8 -3
  114. alita_sdk/tools/zephyr/__init__.py +8 -7
  115. alita_sdk/tools/zephyr_enterprise/__init__.py +10 -8
  116. alita_sdk/tools/zephyr_essential/__init__.py +9 -8
  117. alita_sdk/tools/zephyr_scale/__init__.py +9 -8
  118. alita_sdk/tools/zephyr_squad/__init__.py +9 -8
  119. {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/METADATA +1 -1
  120. {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/RECORD +124 -119
  121. {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/WHEEL +0 -0
  122. {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/entry_points.txt +0 -0
  123. {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/licenses/LICENSE +0 -0
  124. {alita_sdk-0.3.486.dist-info → alita_sdk-0.3.515.dist-info}/top_level.txt +0 -0
@@ -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
- # validated_dict = validated_config.model_dump()
132
- # validated_dict['type'] = toolkit_config.get('type')
133
- # validated_dict['toolkit_name'] = toolkit_config.get('toolkit_name')
134
- # validated_toolkit_configs.append(validated_dict)
135
-
136
- validated_toolkit_configs.append(toolkit_config)
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
 
@@ -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
- Load and parse a toolkit config JSON file.
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
- """Get LLM instance from Alita client context."""
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
- Get configured source toolkit instance from toolkit config.
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
- # Try to get the API wrapper class from the toolkit's module
1216
- import importlib
1217
- module_path = f"alita_sdk.tools.{source}.api_wrapper"
1218
- try:
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
- name = entry['name']
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
- base = Path(self.base_directory).resolve()
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
- rel_path = match.relative_to(base)
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"📁 {rel_path}/")
913
+ results.append(f"📁 {display_path}/")
829
914
  else:
830
915
  size = self._format_size(match.stat().st_size)
831
- results.append(f"📄 {rel_path} ({size})")
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
- output += f"\n\n⚠️ OUTPUT TRUNCATED: Showing {max_results} of {total_count} results (max_results={max_results})"
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, TOOLKIT_SPLITTER, get_max_toolkit_length
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
- # Create tools with toolkit prefix
148
- prefix = clean_string(toolkit_name, cls.toolkit_max_length) + TOOLKIT_SPLITTER if toolkit_name else ''
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=f"{prefix}{tool_name}",
157
- description=tool_info['description'],
160
+ name=tool_name,
161
+ description=description,
158
162
  args_schema=tool_info['args_schema']
159
163
  ))
160
164