alita-sdk 0.3.376__py3-none-any.whl → 0.3.423__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 (50) hide show
  1. alita_sdk/configurations/bitbucket.py +95 -0
  2. alita_sdk/configurations/confluence.py +96 -1
  3. alita_sdk/configurations/gitlab.py +79 -0
  4. alita_sdk/configurations/jira.py +103 -0
  5. alita_sdk/configurations/testrail.py +88 -0
  6. alita_sdk/configurations/xray.py +93 -0
  7. alita_sdk/configurations/zephyr_enterprise.py +93 -0
  8. alita_sdk/configurations/zephyr_essential.py +75 -0
  9. alita_sdk/runtime/clients/client.py +3 -2
  10. alita_sdk/runtime/clients/sandbox_client.py +8 -0
  11. alita_sdk/runtime/langchain/assistant.py +41 -38
  12. alita_sdk/runtime/langchain/constants.py +4 -0
  13. alita_sdk/runtime/langchain/document_loaders/AlitaDocxMammothLoader.py +315 -3
  14. alita_sdk/runtime/langchain/document_loaders/AlitaJSONLoader.py +4 -1
  15. alita_sdk/runtime/langchain/document_loaders/constants.py +28 -12
  16. alita_sdk/runtime/langchain/langraph_agent.py +88 -27
  17. alita_sdk/runtime/langchain/utils.py +24 -4
  18. alita_sdk/runtime/toolkits/application.py +8 -1
  19. alita_sdk/runtime/toolkits/tools.py +80 -49
  20. alita_sdk/runtime/tools/__init__.py +7 -2
  21. alita_sdk/runtime/tools/application.py +7 -0
  22. alita_sdk/runtime/tools/function.py +20 -28
  23. alita_sdk/runtime/tools/graph.py +10 -4
  24. alita_sdk/runtime/tools/image_generation.py +104 -8
  25. alita_sdk/runtime/tools/llm.py +146 -114
  26. alita_sdk/runtime/tools/sandbox.py +166 -63
  27. alita_sdk/runtime/tools/vectorstore.py +3 -2
  28. alita_sdk/runtime/tools/vectorstore_base.py +4 -3
  29. alita_sdk/runtime/utils/utils.py +1 -0
  30. alita_sdk/tools/__init__.py +43 -31
  31. alita_sdk/tools/ado/work_item/ado_wrapper.py +17 -8
  32. alita_sdk/tools/base_indexer_toolkit.py +75 -66
  33. alita_sdk/tools/code_indexer_toolkit.py +13 -3
  34. alita_sdk/tools/confluence/api_wrapper.py +29 -7
  35. alita_sdk/tools/confluence/loader.py +10 -0
  36. alita_sdk/tools/elitea_base.py +7 -7
  37. alita_sdk/tools/gitlab/api_wrapper.py +8 -9
  38. alita_sdk/tools/jira/api_wrapper.py +1 -1
  39. alita_sdk/tools/openapi/__init__.py +10 -1
  40. alita_sdk/tools/qtest/api_wrapper.py +298 -51
  41. alita_sdk/tools/sharepoint/api_wrapper.py +104 -33
  42. alita_sdk/tools/sharepoint/authorization_helper.py +175 -1
  43. alita_sdk/tools/sharepoint/utils.py +8 -2
  44. alita_sdk/tools/utils/content_parser.py +27 -16
  45. alita_sdk/tools/vector_adapters/VectorStoreAdapter.py +19 -6
  46. {alita_sdk-0.3.376.dist-info → alita_sdk-0.3.423.dist-info}/METADATA +1 -1
  47. {alita_sdk-0.3.376.dist-info → alita_sdk-0.3.423.dist-info}/RECORD +50 -50
  48. {alita_sdk-0.3.376.dist-info → alita_sdk-0.3.423.dist-info}/WHEEL +0 -0
  49. {alita_sdk-0.3.376.dist-info → alita_sdk-0.3.423.dist-info}/licenses/LICENSE +0 -0
  50. {alita_sdk-0.3.376.dist-info → alita_sdk-0.3.423.dist-info}/top_level.txt +0 -0
@@ -2,21 +2,60 @@ import asyncio
2
2
  import logging
3
3
  import subprocess
4
4
  import os
5
- from typing import Any, Type, Optional, Dict
6
- from langchain_core.tools import BaseTool
7
- from pydantic import BaseModel, create_model
5
+ from typing import Any, Type, Optional, Dict, List, Literal, Union
6
+ from copy import deepcopy
7
+ from pathlib import Path
8
+
9
+ from langchain_core.tools import BaseTool, BaseToolkit
10
+ from langchain_core.messages import ToolCall
11
+ from pydantic import BaseModel, create_model, ConfigDict, Field
8
12
  from pydantic.fields import FieldInfo
9
13
 
10
14
  logger = logging.getLogger(__name__)
11
15
 
16
+ name = "pyodide"
17
+
18
+
19
+ def get_tools(tools_list: list, alita_client=None, llm=None, memory_store=None):
20
+ """
21
+ Get sandbox tools for the provided tool configurations.
22
+
23
+ Args:
24
+ tools_list: List of tool configurations
25
+ alita_client: Alita client instance for sandbox tools
26
+ llm: LLM client instance (unused for sandbox)
27
+ memory_store: Optional memory store instance (unused for sandbox)
28
+
29
+ Returns:
30
+ List of sandbox tools
31
+ """
32
+ all_tools = []
33
+
34
+ for tool in tools_list:
35
+ if tool.get('type') == 'sandbox' or tool.get('toolkit_name') == 'sandbox':
36
+ try:
37
+ toolkit_instance = SandboxToolkit.get_toolkit(
38
+ stateful=tool['settings'].get('stateful', False),
39
+ allow_net=tool['settings'].get('allow_net', True),
40
+ alita_client=alita_client,
41
+ toolkit_name=tool.get('toolkit_name', '')
42
+ )
43
+ all_tools.extend(toolkit_instance.get_tools())
44
+ except Exception as e:
45
+ logger.error(f"Error in sandbox toolkit get_tools: {e}")
46
+ logger.error(f"Tool config: {tool}")
47
+ raise
48
+
49
+ return all_tools
50
+
12
51
 
13
52
  def _is_deno_available() -> bool:
14
53
  """Check if Deno is available in the PATH"""
15
54
  try:
16
55
  result = subprocess.run(
17
- ["deno", "--version"],
18
- capture_output=True,
19
- text=True,
56
+ ["deno", "--version"],
57
+ capture_output=True,
58
+ text=True,
20
59
  timeout=10
21
60
  )
22
61
  return result.returncode == 0
@@ -25,43 +64,17 @@ def _is_deno_available() -> bool:
25
64
 
26
65
 
27
66
  def _setup_pyodide_cache_env() -> None:
28
- """Setup Pyodide caching environment variables for performance optimization"""
67
+ """Setup Pyodide caching environment variables for performance optimization [NO-OP]"""
29
68
  try:
30
- # Check if cache environment file exists and source it
31
- cache_env_file = os.path.expanduser("~/.pyodide_cache_env")
32
- if os.path.exists(cache_env_file):
33
- with open(cache_env_file, 'r') as f:
34
- for line in f:
35
- line = line.strip()
36
- if line.startswith('export ') and '=' in line:
37
- # Parse export VAR=value format
38
- var_assignment = line[7:] # Remove 'export '
39
- if '=' in var_assignment:
40
- key, value = var_assignment.split('=', 1)
41
- # Remove quotes if present
42
- value = value.strip('"').strip("'")
43
- os.environ[key] = value
44
- logger.debug(f"Set Pyodide cache env: {key}={value}")
45
-
46
- # Set default caching environment variables if not already set
47
- cache_defaults = {
48
- 'PYODIDE_PACKAGES_PATH': os.path.expanduser('~/.cache/pyodide'),
49
- 'DENO_DIR': os.path.expanduser('~/.cache/deno'),
50
- 'PYODIDE_CACHE_DIR': os.path.expanduser('~/.cache/pyodide'),
51
- }
52
-
53
- for key, default_value in cache_defaults.items():
54
- if key not in os.environ:
55
- os.environ[key] = default_value
56
- logger.debug(f"Set default Pyodide env: {key}={default_value}")
57
-
69
+ for key in ["SANDBOX_BASE", "DENO_DIR"]:
70
+ logger.info("Sandbox env: %s -> %s", key, os.environ.get(key, "n/a"))
58
71
  except Exception as e:
59
72
  logger.warning(f"Could not setup Pyodide cache environment: {e}")
60
73
 
61
74
 
62
75
  # Create input schema for the sandbox tool
63
76
  sandbox_tool_input = create_model(
64
- "SandboxToolInput",
77
+ "SandboxToolInput",
65
78
  code=(str, FieldInfo(description="Python code to execute in the sandbox environment"))
66
79
  )
67
80
 
@@ -72,7 +85,7 @@ class PyodideSandboxTool(BaseTool):
72
85
  This tool leverages langchain-sandbox to provide a safe environment for running untrusted Python code.
73
86
  Optimized for performance with caching and stateless execution by default.
74
87
  """
75
-
88
+
76
89
  name: str = "pyodide_sandbox"
77
90
  description: str = """Execute Python code in a secure sandbox environment using Pyodide.
78
91
  This tool allows safe execution of Python code without access to the host system.
@@ -81,7 +94,7 @@ class PyodideSandboxTool(BaseTool):
81
94
  - Perform calculations or data analysis
82
95
  - Test Python algorithms
83
96
  - Run code that requires isolation from the host system
84
-
97
+
85
98
  The sandbox supports most Python standard library modules and can install additional packages.
86
99
  Note: File access and some system operations are restricted for security.
87
100
  Optimized for performance with local caching (stateless by default for faster execution).
@@ -91,14 +104,37 @@ class PyodideSandboxTool(BaseTool):
91
104
  allow_net: bool = True
92
105
  session_bytes: Optional[bytes] = None
93
106
  session_metadata: Optional[Dict] = None
94
-
107
+ alita_client: Optional[Any] = None
108
+
95
109
  def __init__(self, **kwargs: Any) -> None:
96
110
  super().__init__(**kwargs)
97
111
  self._sandbox = None
98
112
  # Setup caching environment for optimal performance
99
113
  _setup_pyodide_cache_env()
100
114
  self._initialize_sandbox()
101
-
115
+
116
+ def _prepare_pyodide_input(self, code: str) -> str:
117
+ """Prepare input for PyodideSandboxTool by injecting state and alita_client into the code block."""
118
+ pyodide_predata = ""
119
+
120
+ # Add alita_client if available
121
+ if self.alita_client:
122
+ try:
123
+ # Get the directory of the current file and construct the path to sandbox_client.py
124
+ current_dir = Path(__file__).parent
125
+ sandbox_client_path = current_dir.parent / 'clients' / 'sandbox_client.py'
126
+
127
+ with open(sandbox_client_path, 'r') as f:
128
+ sandbox_client_code = f.read()
129
+ pyodide_predata += f"{sandbox_client_code}\n"
130
+ pyodide_predata += (f"alita_client = SandboxClient(base_url='{self.alita_client.base_url}',"
131
+ f"project_id={self.alita_client.project_id},"
132
+ f"auth_token='{self.alita_client.auth_token}')\n")
133
+ except FileNotFoundError:
134
+ logger.error(f"sandbox_client.py not found. Ensure the file exists.")
135
+
136
+ return f"#elitea simplified client\n{pyodide_predata}{code}"
137
+
102
138
  def _initialize_sandbox(self) -> None:
103
139
  """Initialize the PyodideSandbox instance with optimized settings"""
104
140
  try:
@@ -110,12 +146,22 @@ class PyodideSandboxTool(BaseTool):
110
146
  )
111
147
  logger.error(error_msg)
112
148
  raise RuntimeError(error_msg)
113
-
149
+
114
150
  from langchain_sandbox import PyodideSandbox
115
-
151
+
152
+ # Air-gapped settings
153
+ sandbox_base = os.environ.get("SANDBOX_BASE", os.path.expanduser('~/.cache/pyodide'))
154
+ sandbox_tmp = os.path.join(sandbox_base, "tmp")
155
+ deno_cache = os.environ.get("DENO_DIR", os.path.expanduser('~/.cache/deno'))
156
+
116
157
  # Configure sandbox with performance optimizations
117
158
  self._sandbox = PyodideSandbox(
118
159
  stateful=self.stateful,
160
+ #
161
+ allow_env=["SANDBOX_BASE"],
162
+ allow_read=[sandbox_base, sandbox_tmp, deno_cache],
163
+ allow_write=[sandbox_tmp, deno_cache],
164
+ #
119
165
  allow_net=self.allow_net,
120
166
  # Use auto node_modules_dir for better caching
121
167
  node_modules_dir="auto"
@@ -135,7 +181,7 @@ class PyodideSandboxTool(BaseTool):
135
181
  except Exception as e:
136
182
  logger.error(f"Failed to initialize PyodideSandbox: {e}")
137
183
  raise
138
-
184
+
139
185
  def _run(self, code: str) -> str:
140
186
  """
141
187
  Synchronous version - runs the async method in a new event loop
@@ -144,7 +190,10 @@ class PyodideSandboxTool(BaseTool):
144
190
  # Check if sandbox is initialized, if not try to initialize
145
191
  if self._sandbox is None:
146
192
  self._initialize_sandbox()
147
-
193
+
194
+ # Prepare code with state and client injection
195
+ prepared_code = self._prepare_pyodide_input(code)
196
+
148
197
  # Check if we're already in an async context
149
198
  try:
150
199
  loop = asyncio.get_running_loop()
@@ -152,11 +201,11 @@ class PyodideSandboxTool(BaseTool):
152
201
  # We'll need to use a different approach
153
202
  import concurrent.futures
154
203
  with concurrent.futures.ThreadPoolExecutor() as executor:
155
- future = executor.submit(asyncio.run, self._arun(code))
204
+ future = executor.submit(asyncio.run, self._arun(prepared_code))
156
205
  return future.result()
157
206
  except RuntimeError:
158
207
  # No running loop, safe to use asyncio.run
159
- return asyncio.run(self._arun(code))
208
+ return asyncio.run(self._arun(prepared_code))
160
209
  except (ImportError, RuntimeError) as e:
161
210
  # Handle specific dependency errors gracefully
162
211
  error_msg = str(e)
@@ -169,7 +218,7 @@ class PyodideSandboxTool(BaseTool):
169
218
  except Exception as e:
170
219
  logger.error(f"Error executing code in sandbox: {e}")
171
220
  return f"Error executing code: {str(e)}"
172
-
221
+
173
222
  async def _arun(self, code: str) -> str:
174
223
  """
175
224
  Execute Python code in the Pyodide sandbox
@@ -177,19 +226,19 @@ class PyodideSandboxTool(BaseTool):
177
226
  try:
178
227
  if self._sandbox is None:
179
228
  self._initialize_sandbox()
180
-
229
+
181
230
  # Execute the code with session state if available
182
231
  result = await self._sandbox.execute(
183
232
  code,
184
233
  session_bytes=self.session_bytes,
185
234
  session_metadata=self.session_metadata
186
235
  )
187
-
236
+
188
237
  # Update session state for stateful execution
189
238
  if self.stateful:
190
239
  self.session_bytes = result.session_bytes
191
240
  self.session_metadata = result.session_metadata
192
-
241
+
193
242
  result_dict = {}
194
243
 
195
244
  if result.result is not None:
@@ -212,10 +261,10 @@ class PyodideSandboxTool(BaseTool):
212
261
 
213
262
  result_dict["execution_info"] = execution_info
214
263
  return result_dict
215
-
264
+
216
265
  except Exception as e:
217
266
  logger.error(f"Error executing code in sandbox: {e}")
218
- return f"Error executing code: {str(e)}"
267
+ return {"error": f"Error executing code: {str(e)}"}
219
268
 
220
269
 
221
270
  class StatefulPyodideSandboxTool(PyodideSandboxTool):
@@ -223,7 +272,7 @@ class StatefulPyodideSandboxTool(PyodideSandboxTool):
223
272
  A stateful version of the PyodideSandboxTool that maintains state between executions.
224
273
  This version preserves variables, imports, and function definitions across multiple tool calls.
225
274
  """
226
-
275
+
227
276
  name: str = "stateful_pyodide_sandbox"
228
277
  description: str = """Execute Python code in a stateful sandbox environment using Pyodide.
229
278
  This tool maintains state between executions, preserving variables, imports, and function definitions.
@@ -232,41 +281,95 @@ class StatefulPyodideSandboxTool(PyodideSandboxTool):
232
281
  - Maintain variables across multiple calls
233
282
  - Develop complex programs step by step
234
283
  - Preserve imported libraries and defined functions
235
-
284
+
236
285
  The sandbox supports most Python standard library modules and can install additional packages.
237
286
  Note: File access and some system operations are restricted for security.
238
287
  """
239
-
288
+
240
289
  def __init__(self, **kwargs: Any) -> None:
241
290
  kwargs['stateful'] = True # Force stateful mode
242
291
  super().__init__(**kwargs)
243
292
 
244
293
 
245
294
  # Factory function for creating sandbox tools
246
- def create_sandbox_tool(stateful: bool = False, allow_net: bool = True) -> BaseTool:
295
+ def create_sandbox_tool(stateful: bool = False, allow_net: bool = True, alita_client: Optional[Any] = None) -> BaseTool:
247
296
  """
248
297
  Factory function to create sandbox tools with specified configuration.
249
-
298
+
250
299
  Note: This tool requires Deno to be installed and available in PATH.
251
300
  For installation and optimization, run the bootstrap.sh script.
252
-
301
+
253
302
  Args:
254
303
  stateful: Whether to maintain state between executions (default: False for better performance)
255
304
  allow_net: Whether to allow network access (for package installation)
256
-
305
+
257
306
  Returns:
258
307
  Configured sandbox tool instance
259
-
308
+
260
309
  Raises:
261
310
  ImportError: If langchain-sandbox is not installed
262
311
  RuntimeError: If Deno is not found in PATH
263
-
312
+
264
313
  Performance Notes:
265
314
  - Stateless mode (default) is faster and avoids session state overhead
266
315
  - Run bootstrap.sh script to enable local caching and reduce initialization time
267
316
  - Cached wheels reduce package download time from ~4.76s to near-instant
268
317
  """
269
318
  if stateful:
270
- return StatefulPyodideSandboxTool(allow_net=allow_net)
319
+ return StatefulPyodideSandboxTool(allow_net=allow_net, alita_client=alita_client)
271
320
  else:
272
- return PyodideSandboxTool(stateful=False, allow_net=allow_net)
321
+ return PyodideSandboxTool(stateful=False, allow_net=allow_net, alita_client=alita_client)
322
+
323
+
324
+ class SandboxToolkit(BaseToolkit):
325
+ tools: List[BaseTool] = []
326
+
327
+ @staticmethod
328
+ def toolkit_config_schema() -> Type[BaseModel]:
329
+ # Create sample tools to get their schemas
330
+ sample_tools = [
331
+ PyodideSandboxTool(),
332
+ StatefulPyodideSandboxTool()
333
+ ]
334
+ selected_tools = {x.name: x.args_schema.model_json_schema() for x in sample_tools}
335
+
336
+ return create_model(
337
+ 'sandbox',
338
+ stateful=(bool, Field(default=False, description="Whether to maintain state between executions")),
339
+ allow_net=(bool, Field(default=True, description="Whether to allow network access for package installation")),
340
+ selected_tools=(List[Literal[tuple(selected_tools)]],
341
+ Field(default=[], json_schema_extra={'args_schemas': selected_tools})),
342
+
343
+ __config__=ConfigDict(json_schema_extra={
344
+ 'metadata': {
345
+ "label": "Python Sandbox",
346
+ "icon_url": "sandbox.svg",
347
+ "hidden": False,
348
+ "categories": ["code", "execution", "internal_tool"],
349
+ "extra_categories": ["python", "pyodide", "sandbox", "code execution"],
350
+ }
351
+ })
352
+ )
353
+
354
+ @classmethod
355
+ def get_toolkit(cls, stateful: bool = False, allow_net: bool = True, alita_client=None, **kwargs):
356
+ """
357
+ Get toolkit with sandbox tools.
358
+
359
+ Args:
360
+ stateful: Whether to maintain state between executions
361
+ allow_net: Whether to allow network access
362
+ alita_client: Alita client instance for sandbox tools
363
+ **kwargs: Additional arguments
364
+ """
365
+ tools = []
366
+
367
+ if stateful:
368
+ tools.append(StatefulPyodideSandboxTool(allow_net=allow_net, alita_client=alita_client))
369
+ else:
370
+ tools.append(PyodideSandboxTool(stateful=False, allow_net=allow_net, alita_client=alita_client))
371
+
372
+ return cls(tools=tools)
373
+
374
+ def get_tools(self):
375
+ return self.tools
@@ -215,7 +215,7 @@ class VectorStoreWrapper(BaseToolApiWrapper):
215
215
  """List all collections in the vectorstore.
216
216
  Returns a list of collection names, or if no collections exist,
217
217
  returns a dict with an empty list and a message."""
218
- raw = self.vector_adapter.list_indexes(self)
218
+ raw = self.vector_adapter.list_collections(self)
219
219
  # Normalize raw result to a list of names
220
220
  if not raw:
221
221
  # No collections found
@@ -414,7 +414,8 @@ class VectorStoreWrapper(BaseToolApiWrapper):
414
414
  return {"status": "error", "message": f"Error: {format_exc()}"}
415
415
  if _documents:
416
416
  add_documents(vectorstore=self.vectorstore, documents=_documents)
417
- return {"status": "ok", "message": f"successfully indexed {documents_count} documents"}
417
+ return {"status": "ok", "message": f"successfully indexed {documents_count} documents" if documents_count > 0
418
+ else "No new documents to index."}
418
419
 
419
420
  def search_documents(self, query:str, doctype: str = 'code',
420
421
  filter:dict|str={}, cut_off: float=0.5,
@@ -208,10 +208,10 @@ class VectorStoreWrapperBase(BaseToolApiWrapper):
208
208
  logger.error(f"Error during similarity search: {str(e)}")
209
209
  raise ToolException(f"Search failed: {str(e)}")
210
210
 
211
- def list_indexes(self) -> List[str]:
211
+ def list_collections(self) -> List[str]:
212
212
  """List all collections in the vectorstore."""
213
213
 
214
- collections = self.vector_adapter.list_indexes(self)
214
+ collections = self.vector_adapter.list_collections(self)
215
215
  if not collections:
216
216
  return "No indexed collections"
217
217
  return collections
@@ -308,7 +308,8 @@ class VectorStoreWrapperBase(BaseToolApiWrapper):
308
308
  return {"status": "error", "message": f"Error: {format_exc()}"}
309
309
  if _documents:
310
310
  add_documents(vectorstore=self.vectorstore, documents=_documents)
311
- return {"status": "ok", "message": f"successfully indexed {documents_count} documents"}
311
+ return {"status": "ok", "message": f"successfully indexed {documents_count} documents" if documents_count > 0
312
+ else "no documents to index"}
312
313
 
313
314
  def search_documents(self, query:str, doctype: str = 'code',
314
315
  filter:dict|str={}, cut_off: float=0.5,
@@ -14,6 +14,7 @@ class IndexerKeywords(Enum):
14
14
  INDEX_META_TYPE = 'index_meta'
15
15
  INDEX_META_IN_PROGRESS = 'in_progress'
16
16
  INDEX_META_COMPLETED = 'completed'
17
+ INDEX_META_FAILED = 'failed'
17
18
 
18
19
  # This pattern matches characters that are NOT alphanumeric, underscores, or hyphens
19
20
  clean_string_pattern = re.compile(r'[^a-zA-Z0-9_.-]')
@@ -90,62 +90,74 @@ available_count = len(AVAILABLE_TOOLS)
90
90
  total_attempted = len(AVAILABLE_TOOLS) + len(FAILED_IMPORTS)
91
91
  logger.info(f"Tool imports completed: {available_count}/{total_attempted} successful")
92
92
 
93
+
93
94
  def get_tools(tools_list, alita, llm, store: Optional[BaseStore] = None, *args, **kwargs):
94
95
  tools = []
96
+
95
97
  for tool in tools_list:
96
- # validate tool name syntax - it cannot be started with _
97
- for tool_name in tool.get('settings', {}).get('selected_tools', []):
98
- if isinstance(tool_name, str) and tool_name.startswith('_'):
99
- raise ValueError(f"Tool name '{tool_name}' from toolkit '{tool.get('type', '')}' cannot start with '_'")
100
-
101
- tool['settings']['alita'] = alita
102
- tool['settings']['llm'] = llm
103
- tool['settings']['store'] = store
98
+ settings = tool.get('settings')
99
+
100
+ # Skip tools without settings early
101
+ if not settings:
102
+ logger.warning(f"Tool '{tool.get('type', '')}' has no settings, skipping...")
103
+ continue
104
+
105
+ # Validate tool names once
106
+ selected_tools = settings.get('selected_tools', [])
107
+ invalid_tools = [name for name in selected_tools if isinstance(name, str) and name.startswith('_')]
108
+ if invalid_tools:
109
+ raise ValueError(f"Tool names {invalid_tools} from toolkit '{tool.get('type', '')}' cannot start with '_'")
110
+
111
+ # Cache tool type and add common settings
104
112
  tool_type = tool['type']
113
+ settings['alita'] = alita
114
+ settings['llm'] = llm
115
+ settings['store'] = store
116
+
117
+ # Set pgvector collection schema if present
118
+ if settings.get('pgvector_configuration'):
119
+ settings['pgvector_configuration']['collection_schema'] = str(tool['id'])
105
120
 
106
- # Handle special cases for ADO tools
121
+ # Handle ADO special cases
107
122
  if tool_type in ['ado_boards', 'ado_wiki', 'ado_plans']:
108
123
  tools.extend(AVAILABLE_TOOLS['ado']['get_tools'](tool_type, tool))
124
+ continue
109
125
 
110
- # Check if tool is available and has get_tools function
111
- elif tool_type in AVAILABLE_TOOLS and 'get_tools' in AVAILABLE_TOOLS[tool_type]:
126
+ # Handle ADO repos aliases
127
+ if tool_type in ['ado_repos', 'azure_devops_repos'] and 'ado_repos' in AVAILABLE_TOOLS:
112
128
  try:
113
- get_tools_func = AVAILABLE_TOOLS[tool_type]['get_tools']
114
- tools.extend(get_tools_func(tool))
115
-
129
+ tools.extend(AVAILABLE_TOOLS['ado_repos']['get_tools'](tool))
116
130
  except Exception as e:
117
- logger.error(f"Error getting tools for {tool_type}: {e}")
118
- raise ToolException(f"Error getting tools for {tool_type}: {e}")
131
+ logger.error(f"Error getting ADO repos tools: {e}")
132
+ continue
119
133
 
120
- # Handle ADO repos special case (it might be requested as azure_devops_repos)
121
- elif tool_type in ['ado_repos', 'azure_devops_repos'] and 'ado_repos' in AVAILABLE_TOOLS:
134
+ # Handle standard tools
135
+ if tool_type in AVAILABLE_TOOLS and 'get_tools' in AVAILABLE_TOOLS[tool_type]:
122
136
  try:
123
- get_tools_func = AVAILABLE_TOOLS['ado_repos']['get_tools']
124
- tools.extend(get_tools_func(tool))
137
+ tools.extend(AVAILABLE_TOOLS[tool_type]['get_tools'](tool))
125
138
  except Exception as e:
126
- logger.error(f"Error getting ADO repos tools: {e}")
139
+ logger.error(f"Error getting tools for {tool_type}: {e}")
140
+ raise ToolException(f"Error getting tools for {tool_type}: {e}")
141
+ continue
127
142
 
128
143
  # Handle custom modules
129
- elif tool.get("settings", {}).get("module"):
144
+ if settings.get("module"):
130
145
  try:
131
- settings = tool.get("settings", {})
132
146
  mod = import_module(settings.pop("module"))
133
147
  tkitclass = getattr(mod, settings.pop("class"))
134
- #
135
- get_toolkit_params = tool["settings"].copy()
148
+ get_toolkit_params = settings.copy()
136
149
  get_toolkit_params["name"] = tool.get("name")
137
- #
138
150
  toolkit = tkitclass.get_toolkit(**get_toolkit_params)
139
151
  tools.extend(toolkit.get_tools())
140
152
  except Exception as e:
141
153
  logger.error(f"Error in getting custom toolkit: {e}")
154
+ continue
142
155
 
156
+ # Tool not available
157
+ if tool_type in FAILED_IMPORTS:
158
+ logger.warning(f"Tool '{tool_type}' is not available: {FAILED_IMPORTS[tool_type]}")
143
159
  else:
144
- # Tool not available or not found
145
- if tool_type in FAILED_IMPORTS:
146
- logger.warning(f"Tool '{tool_type}' is not available: {FAILED_IMPORTS[tool_type]}")
147
- else:
148
- logger.warning(f"Unknown tool type: {tool_type}")
160
+ logger.warning(f"Unknown tool type: {tool_type}")
149
161
 
150
162
  return tools
151
163
 
@@ -329,11 +329,14 @@ class AzureDevOpsApiWrapper(NonCodeIndexerToolkit):
329
329
  parsed_item.update(fields_data)
330
330
 
331
331
  # extract relations if any
332
- relations_data = work_item.relations
332
+ relations_data = None
333
+ if expand and str(expand).lower() in ("relations", "all"):
334
+ try:
335
+ relations_data = getattr(work_item, 'relations', None)
336
+ except KeyError:
337
+ relations_data = None
333
338
  if relations_data:
334
- parsed_item['relations'] = []
335
- for relation in relations_data:
336
- parsed_item['relations'].append(relation.as_dict())
339
+ parsed_item['relations'] = [relation.as_dict() for relation in relations_data]
337
340
 
338
341
  if parse_attachments:
339
342
  # describe images in work item fields if present
@@ -344,13 +347,19 @@ class AzureDevOpsApiWrapper(NonCodeIndexerToolkit):
344
347
  for img in images:
345
348
  src = img.get('src')
346
349
  if src:
347
- description = self.parse_attachment_by_url(src, image_description_prompt)
350
+ description = self.parse_attachment_by_url(src, image_description_prompt=image_description_prompt)
348
351
  img['image-description'] = description
349
352
  parsed_item[field_name] = str(soup)
350
353
  # parse attached documents if present
351
- if parsed_item['relations']:
352
- for attachment in parsed_item['relations']:
353
- attachment['content'] = self.parse_attachment_by_url(attachment['url'], attachment['attributes']['name'], image_description_prompt)
354
+ for relation in parsed_item.get('relations', []):
355
+ # Only process actual file attachments
356
+ if relation.get('rel') == 'AttachedFile':
357
+ file_name = relation.get('attributes', {}).get('name')
358
+ if file_name:
359
+ try:
360
+ relation['content'] = self.parse_attachment_by_url(relation['url'], file_name, image_description_prompt=image_description_prompt)
361
+ except Exception as att_e:
362
+ logger.warning(f"Failed to parse attachment {file_name}: {att_e}")
354
363
 
355
364
 
356
365
  return parsed_item