alita-sdk 0.3.603__py3-none-any.whl → 0.3.611__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/agents.py +108 -826
- alita_sdk/cli/testcases/__init__.py +94 -0
- alita_sdk/cli/testcases/data_generation.py +119 -0
- alita_sdk/cli/testcases/discovery.py +96 -0
- alita_sdk/cli/testcases/executor.py +84 -0
- alita_sdk/cli/testcases/logger.py +85 -0
- alita_sdk/cli/testcases/parser.py +172 -0
- alita_sdk/cli/testcases/prompts.py +91 -0
- alita_sdk/cli/testcases/reporting.py +125 -0
- alita_sdk/cli/testcases/setup.py +108 -0
- alita_sdk/cli/testcases/test_runner.py +282 -0
- alita_sdk/cli/testcases/utils.py +39 -0
- alita_sdk/cli/testcases/validation.py +90 -0
- alita_sdk/cli/testcases/workflow.py +196 -0
- alita_sdk/configurations/openapi.py +2 -2
- alita_sdk/runtime/clients/artifact.py +1 -1
- alita_sdk/runtime/langchain/langraph_agent.py +21 -6
- alita_sdk/runtime/tools/artifact.py +253 -8
- alita_sdk/runtime/tools/function.py +25 -6
- alita_sdk/runtime/tools/llm.py +12 -11
- alita_sdk/runtime/utils/serialization.py +155 -0
- alita_sdk/tools/bitbucket/api_wrapper.py +31 -30
- alita_sdk/tools/bitbucket/cloud_api_wrapper.py +49 -35
- alita_sdk/tools/confluence/api_wrapper.py +8 -1
- alita_sdk/tools/elitea_base.py +40 -36
- alita_sdk/tools/figma/api_wrapper.py +140 -83
- alita_sdk/tools/github/graphql_client_wrapper.py +1 -0
- alita_sdk/tools/utils/text_operations.py +156 -52
- {alita_sdk-0.3.603.dist-info → alita_sdk-0.3.611.dist-info}/METADATA +1 -1
- {alita_sdk-0.3.603.dist-info → alita_sdk-0.3.611.dist-info}/RECORD +34 -20
- {alita_sdk-0.3.603.dist-info → alita_sdk-0.3.611.dist-info}/WHEEL +0 -0
- {alita_sdk-0.3.603.dist-info → alita_sdk-0.3.611.dist-info}/entry_points.txt +0 -0
- {alita_sdk-0.3.603.dist-info → alita_sdk-0.3.611.dist-info}/licenses/LICENSE +0 -0
- {alita_sdk-0.3.603.dist-info → alita_sdk-0.3.611.dist-info}/top_level.txt +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import base64
|
|
1
2
|
import json
|
|
2
3
|
import logging
|
|
3
4
|
from copy import deepcopy
|
|
@@ -12,6 +13,7 @@ from langchain_core.utils.function_calling import convert_to_openai_tool
|
|
|
12
13
|
from pydantic import ValidationError
|
|
13
14
|
|
|
14
15
|
from ..langchain.utils import propagate_the_input_mapping
|
|
16
|
+
from ..utils.serialization import safe_serialize
|
|
15
17
|
|
|
16
18
|
logger = logging.getLogger(__name__)
|
|
17
19
|
|
|
@@ -40,15 +42,32 @@ class FunctionTool(BaseTool):
|
|
|
40
42
|
alita_client: Optional[Any] = None
|
|
41
43
|
|
|
42
44
|
def _prepare_pyodide_input(self, state: Union[str, dict, ToolCall]) -> str:
|
|
43
|
-
"""Prepare input for PyodideSandboxTool by injecting state into the code block.
|
|
44
|
-
|
|
45
|
+
"""Prepare input for PyodideSandboxTool by injecting state into the code block.
|
|
46
|
+
|
|
47
|
+
Uses base64 encoding to avoid string escaping issues when passing JSON
|
|
48
|
+
through multiple layers of parsing (Python -> Deno -> Pyodide).
|
|
49
|
+
"""
|
|
45
50
|
state_copy = replace_escaped_newlines(deepcopy(state))
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
# remove messages to avoid issues with pickling without langchain-core
|
|
53
|
+
if 'messages' in state_copy:
|
|
54
|
+
del state_copy['messages']
|
|
55
|
+
|
|
56
|
+
# Use safe_serialize to handle Pydantic models, datetime, and other non-JSON types
|
|
57
|
+
state_json = safe_serialize(state_copy)
|
|
51
58
|
|
|
59
|
+
# Use base64 encoding to avoid all string escaping issues
|
|
60
|
+
# This is more robust than repr() when the code passes through multiple parsers
|
|
61
|
+
state_json_b64 = base64.b64encode(state_json.encode('utf-8')).decode('ascii')
|
|
62
|
+
|
|
63
|
+
# Generate code that decodes base64 and parses JSON inside Pyodide
|
|
64
|
+
pyodide_predata = f'''#state dict
|
|
65
|
+
import json
|
|
66
|
+
import base64
|
|
67
|
+
_state_json_b64 = "{state_json_b64}"
|
|
68
|
+
_state_json = base64.b64decode(_state_json_b64).decode('utf-8')
|
|
69
|
+
alita_state = json.loads(_state_json)
|
|
70
|
+
'''
|
|
52
71
|
return pyodide_predata
|
|
53
72
|
|
|
54
73
|
def _handle_pyodide_output(self, tool_result: Any) -> dict:
|
alita_sdk/runtime/tools/llm.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
3
|
from traceback import format_exc
|
|
4
|
-
from typing import Any, Optional, List, Union, Literal
|
|
4
|
+
from typing import Any, Optional, List, Union, Literal, Dict
|
|
5
5
|
|
|
6
6
|
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
|
|
7
7
|
from langchain_core.runnables import RunnableConfig
|
|
@@ -67,7 +67,7 @@ class LLMNode(BaseTool):
|
|
|
67
67
|
client: Any = Field(default=None, description='LLM client instance')
|
|
68
68
|
return_type: str = Field(default="str", description='Return type')
|
|
69
69
|
response_key: str = Field(default="messages", description='Response key')
|
|
70
|
-
structured_output_dict: Optional[
|
|
70
|
+
structured_output_dict: Optional[Dict[str, Any]] = Field(default=None, description='Structured output dictionary')
|
|
71
71
|
output_variables: Optional[List[str]] = Field(default=None, description='Output variables')
|
|
72
72
|
input_mapping: Optional[dict[str, dict]] = Field(default=None, description='Input mapping')
|
|
73
73
|
input_variables: Optional[List[str]] = Field(default=None, description='Input variables')
|
|
@@ -82,7 +82,7 @@ class LLMNode(BaseTool):
|
|
|
82
82
|
Prepare structured output parameters from structured_output_dict.
|
|
83
83
|
|
|
84
84
|
Expected self.structured_output_dict formats:
|
|
85
|
-
- {"field": "str"} / {"field": "list"} / {"field": "list[
|
|
85
|
+
- {"field": "str"} / {"field": "list"} / {"field": "list[dict]"} / {"field": "any"} ...
|
|
86
86
|
- OR {"field": {"type": "...", "description": "...", "default": ...}} (optional)
|
|
87
87
|
|
|
88
88
|
Returns:
|
|
@@ -93,19 +93,20 @@ class LLMNode(BaseTool):
|
|
|
93
93
|
for key, value in (self.structured_output_dict or {}).items():
|
|
94
94
|
# Allow either a plain type string or a dict with details
|
|
95
95
|
if isinstance(value, dict):
|
|
96
|
-
type_str = (value.get("type") or "any")
|
|
96
|
+
type_str = str(value.get("type") or "any")
|
|
97
97
|
desc = value.get("description", "") or ""
|
|
98
98
|
entry: dict = {"type": type_str, "description": desc}
|
|
99
99
|
if "default" in value:
|
|
100
100
|
entry["default"] = value["default"]
|
|
101
101
|
else:
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
# Ensure we always have a string type
|
|
103
|
+
if isinstance(value, str):
|
|
104
|
+
type_str = value
|
|
105
|
+
else:
|
|
106
|
+
# If it's already a type object, convert to string representation
|
|
107
|
+
type_str = getattr(value, '__name__', 'any')
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
# (avoid the old bug where "if 'list' in value" also hits "blacklist", etc.)
|
|
107
|
-
if isinstance(entry.get("type"), str) and entry["type"].strip().lower() == "list":
|
|
108
|
-
entry["type"] = "list[str]"
|
|
109
|
+
entry = {"type": type_str, "description": ""}
|
|
109
110
|
|
|
110
111
|
struct_params[key] = entry
|
|
111
112
|
|
|
@@ -1146,5 +1147,5 @@ class LLMNode(BaseTool):
|
|
|
1146
1147
|
|
|
1147
1148
|
return new_messages, current_completion
|
|
1148
1149
|
|
|
1149
|
-
def __get_struct_output_model(self, llm_client, pydantic_model, method: Literal["function_calling", "json_mode", "json_schema"] = "
|
|
1150
|
+
def __get_struct_output_model(self, llm_client, pydantic_model, method: Literal["function_calling", "json_mode", "json_schema"] = "function_calling"):
|
|
1150
1151
|
return llm_client.with_structured_output(pydantic_model, method=method)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Serialization utilities for safe JSON encoding of complex objects.
|
|
3
|
+
|
|
4
|
+
Handles Pydantic models, LangChain messages, datetime objects, and other
|
|
5
|
+
non-standard types that may appear in state variables.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
from datetime import datetime, date
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _convert_to_serializable(obj: Any, _seen: set = None) -> Any:
|
|
16
|
+
"""
|
|
17
|
+
Recursively convert an object to JSON-serializable primitives.
|
|
18
|
+
|
|
19
|
+
Handles nested dicts and lists that may contain non-serializable objects.
|
|
20
|
+
Uses a seen set to prevent infinite recursion with circular references.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
obj: Any object to convert
|
|
24
|
+
_seen: Internal set to track seen object ids (for circular reference detection)
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
JSON-serializable representation of the object
|
|
28
|
+
"""
|
|
29
|
+
# Initialize seen set for circular reference detection
|
|
30
|
+
if _seen is None:
|
|
31
|
+
_seen = set()
|
|
32
|
+
|
|
33
|
+
# Check for circular references (only for mutable objects)
|
|
34
|
+
obj_id = id(obj)
|
|
35
|
+
if isinstance(obj, (dict, list, set)) and obj_id in _seen:
|
|
36
|
+
return f"<circular reference: {type(obj).__name__}>"
|
|
37
|
+
|
|
38
|
+
# Primitives - return as-is
|
|
39
|
+
if obj is None or isinstance(obj, (str, int, float, bool)):
|
|
40
|
+
return obj
|
|
41
|
+
|
|
42
|
+
# Add to seen set for mutable containers
|
|
43
|
+
if isinstance(obj, (dict, list, set)):
|
|
44
|
+
_seen = _seen | {obj_id} # Create new set to avoid mutation issues
|
|
45
|
+
|
|
46
|
+
# Dict - recursively process all values
|
|
47
|
+
if isinstance(obj, dict):
|
|
48
|
+
return {
|
|
49
|
+
_convert_to_serializable(k, _seen): _convert_to_serializable(v, _seen)
|
|
50
|
+
for k, v in obj.items()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# List/tuple - recursively process all items
|
|
54
|
+
if isinstance(obj, (list, tuple)):
|
|
55
|
+
return [_convert_to_serializable(item, _seen) for item in obj]
|
|
56
|
+
|
|
57
|
+
# Set - convert to list and process
|
|
58
|
+
if isinstance(obj, set):
|
|
59
|
+
return [_convert_to_serializable(item, _seen) for item in obj]
|
|
60
|
+
|
|
61
|
+
# Bytes - decode to string
|
|
62
|
+
if isinstance(obj, bytes):
|
|
63
|
+
try:
|
|
64
|
+
return obj.decode('utf-8')
|
|
65
|
+
except UnicodeDecodeError:
|
|
66
|
+
return obj.decode('utf-8', errors='replace')
|
|
67
|
+
|
|
68
|
+
# Datetime objects
|
|
69
|
+
if isinstance(obj, datetime):
|
|
70
|
+
return obj.isoformat()
|
|
71
|
+
if isinstance(obj, date):
|
|
72
|
+
return obj.isoformat()
|
|
73
|
+
|
|
74
|
+
# Pydantic BaseModel (v2) - check for model_dump method
|
|
75
|
+
if hasattr(obj, 'model_dump') and callable(getattr(obj, 'model_dump')):
|
|
76
|
+
try:
|
|
77
|
+
return _convert_to_serializable(obj.model_dump(), _seen)
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.debug(f"Failed to call model_dump on {type(obj).__name__}: {e}")
|
|
80
|
+
|
|
81
|
+
# Pydantic BaseModel (v1) - check for dict method
|
|
82
|
+
if hasattr(obj, 'dict') and callable(getattr(obj, 'dict')) and hasattr(obj, '__fields__'):
|
|
83
|
+
try:
|
|
84
|
+
return _convert_to_serializable(obj.dict(), _seen)
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.debug(f"Failed to call dict on {type(obj).__name__}: {e}")
|
|
87
|
+
|
|
88
|
+
# LangChain BaseMessage - extract key fields
|
|
89
|
+
if hasattr(obj, 'type') and hasattr(obj, 'content'):
|
|
90
|
+
try:
|
|
91
|
+
result = {
|
|
92
|
+
"type": obj.type,
|
|
93
|
+
"content": _convert_to_serializable(obj.content, _seen),
|
|
94
|
+
}
|
|
95
|
+
if hasattr(obj, 'additional_kwargs') and obj.additional_kwargs:
|
|
96
|
+
result["additional_kwargs"] = _convert_to_serializable(obj.additional_kwargs, _seen)
|
|
97
|
+
if hasattr(obj, 'name') and obj.name:
|
|
98
|
+
result["name"] = obj.name
|
|
99
|
+
return result
|
|
100
|
+
except Exception as e:
|
|
101
|
+
logger.debug(f"Failed to extract message fields from {type(obj).__name__}: {e}")
|
|
102
|
+
|
|
103
|
+
# Objects with __dict__ attribute (custom classes)
|
|
104
|
+
if hasattr(obj, '__dict__'):
|
|
105
|
+
try:
|
|
106
|
+
return _convert_to_serializable(obj.__dict__, _seen)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.debug(f"Failed to serialize __dict__ of {type(obj).__name__}: {e}")
|
|
109
|
+
|
|
110
|
+
# UUID objects
|
|
111
|
+
if hasattr(obj, 'hex') and hasattr(obj, 'int'):
|
|
112
|
+
return str(obj)
|
|
113
|
+
|
|
114
|
+
# Enum objects
|
|
115
|
+
if hasattr(obj, 'value') and hasattr(obj, 'name') and hasattr(obj.__class__, '__members__'):
|
|
116
|
+
return obj.value
|
|
117
|
+
|
|
118
|
+
# Last resort - convert to string
|
|
119
|
+
try:
|
|
120
|
+
return str(obj)
|
|
121
|
+
except Exception:
|
|
122
|
+
return f"<non-serializable: {type(obj).__name__}>"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def safe_serialize(obj: Any, **kwargs) -> str:
|
|
126
|
+
"""
|
|
127
|
+
Safely serialize any object to a JSON string.
|
|
128
|
+
|
|
129
|
+
Pre-processes the entire object tree to convert non-serializable
|
|
130
|
+
objects before passing to json.dumps. This ensures nested dicts
|
|
131
|
+
and lists with non-standard objects are handled correctly.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
obj: Any object to serialize
|
|
135
|
+
**kwargs: Additional arguments passed to json.dumps
|
|
136
|
+
(e.g., indent, sort_keys)
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
JSON string representation of the object
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
>>> from pydantic import BaseModel
|
|
143
|
+
>>> class User(BaseModel):
|
|
144
|
+
... name: str
|
|
145
|
+
>>> state = {"user": User(name="Alice"), "count": 5}
|
|
146
|
+
>>> safe_serialize(state)
|
|
147
|
+
'{"user": {"name": "Alice"}, "count": 5}'
|
|
148
|
+
"""
|
|
149
|
+
# Pre-process the entire object tree
|
|
150
|
+
serializable = _convert_to_serializable(obj)
|
|
151
|
+
|
|
152
|
+
# Set defaults
|
|
153
|
+
kwargs.setdefault('ensure_ascii', False)
|
|
154
|
+
|
|
155
|
+
return json.dumps(serializable, **kwargs)
|
|
@@ -270,11 +270,20 @@ class BitbucketAPIWrapper(CodeIndexerToolkit):
|
|
|
270
270
|
>>>> NEW
|
|
271
271
|
branch(str): branch name (by default: active_branch)
|
|
272
272
|
Returns:
|
|
273
|
-
str: A success or failure
|
|
273
|
+
str | ToolException: A success message or a ToolException on failure.
|
|
274
274
|
"""
|
|
275
275
|
try:
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
# Use the shared edit_file logic from BaseCodeToolApiWrapper, operating on
|
|
277
|
+
# this wrapper instance, which provides _read_file and _write_file.
|
|
278
|
+
result = self.edit_file(
|
|
279
|
+
file_path=file_path,
|
|
280
|
+
branch=branch,
|
|
281
|
+
file_query=update_query,
|
|
282
|
+
)
|
|
283
|
+
return result
|
|
284
|
+
except ToolException as e:
|
|
285
|
+
# Pass through ToolExceptions as-is so callers can handle them uniformly.
|
|
286
|
+
return e
|
|
278
287
|
except Exception as e:
|
|
279
288
|
return ToolException(f"File was not updated due to error: {str(e)}")
|
|
280
289
|
|
|
@@ -415,37 +424,29 @@ class BitbucketAPIWrapper(CodeIndexerToolkit):
|
|
|
415
424
|
file_path: str,
|
|
416
425
|
content: str,
|
|
417
426
|
branch: str = None,
|
|
418
|
-
commit_message: str = None
|
|
427
|
+
commit_message: str = None,
|
|
419
428
|
) -> str:
|
|
429
|
+
"""Write content to a file (create or update) via the underlying Bitbucket client.
|
|
430
|
+
|
|
431
|
+
This delegates to the low-level BitbucketServerApi/BitbucketCloudApi `_write_file`
|
|
432
|
+
implementations, so all backend-specific commit behavior (server vs cloud) is
|
|
433
|
+
centralized there. Used by BaseCodeToolApiWrapper.edit_file.
|
|
420
434
|
"""
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
Parameters:
|
|
424
|
-
file_path: Path to the file
|
|
425
|
-
content: New file content
|
|
426
|
-
branch: Branch name (uses active branch if None)
|
|
427
|
-
commit_message: Commit message (not used by Bitbucket API)
|
|
428
|
-
|
|
429
|
-
Returns:
|
|
430
|
-
Success message
|
|
431
|
-
"""
|
|
435
|
+
branch = branch or self._active_branch
|
|
432
436
|
try:
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
# File doesn't exist, create it
|
|
445
|
-
self._bitbucket.create_file(file_path=file_path, file_contents=content, branch=branch)
|
|
446
|
-
return f"Created file {file_path}"
|
|
437
|
+
# Delegate actual write/commit to the underlying API wrapper, which
|
|
438
|
+
# implements _write_file(file_path, content, branch, commit_message).
|
|
439
|
+
self._bitbucket._write_file(
|
|
440
|
+
file_path=file_path,
|
|
441
|
+
content=content,
|
|
442
|
+
branch=branch,
|
|
443
|
+
commit_message=commit_message or f"Update {file_path}",
|
|
444
|
+
)
|
|
445
|
+
return f"Update {file_path}"
|
|
446
|
+
except ToolException:
|
|
447
|
+
raise
|
|
447
448
|
except Exception as e:
|
|
448
|
-
raise ToolException(f"Unable to write file {file_path}: {str(e)}")
|
|
449
|
+
raise ToolException(f"Unable to write file {file_path} on branch {branch}: {str(e)}")
|
|
449
450
|
|
|
450
451
|
@extend_with_parent_available_tools
|
|
451
452
|
@extend_with_file_operations
|
|
@@ -142,32 +142,28 @@ class BitbucketServerApi(BitbucketApiAbstract):
|
|
|
142
142
|
filename=file_path
|
|
143
143
|
)
|
|
144
144
|
|
|
145
|
-
def
|
|
146
|
-
|
|
147
|
-
updated_file_content = file_content
|
|
148
|
-
for old, new in parse_old_new_markers(update_query):
|
|
149
|
-
if not old.strip():
|
|
150
|
-
continue
|
|
151
|
-
updated_file_content = updated_file_content.replace(old, new)
|
|
152
|
-
|
|
153
|
-
if file_content == updated_file_content:
|
|
154
|
-
raise ToolException(
|
|
155
|
-
"File content was not updated because old content was not found or empty. "
|
|
156
|
-
"It may be helpful to use the read_file action to get "
|
|
157
|
-
"the current file contents."
|
|
158
|
-
)
|
|
145
|
+
def _write_file(self, file_path: str, content: str, branch: str, commit_message: str) -> str:
|
|
146
|
+
"""Write updated file content to Bitbucket Server.
|
|
159
147
|
|
|
148
|
+
it creates a new commit on the given branch that edits the existing file.
|
|
149
|
+
"""
|
|
150
|
+
# Get the latest commit on the branch (used as source_commit_id)
|
|
160
151
|
source_commit_generator = self.api_client.get_commits(project_key=self.project, repository_slug=self.repository,
|
|
161
152
|
hash_newest=branch, limit=1)
|
|
162
|
-
source_commit = next(source_commit_generator)
|
|
153
|
+
source_commit = next(source_commit_generator, None)
|
|
154
|
+
if not source_commit:
|
|
155
|
+
raise ToolException(
|
|
156
|
+
f"Unable to determine latest commit on branch '{branch}' for repository '{self.repository}'."
|
|
157
|
+
)
|
|
158
|
+
|
|
163
159
|
return self.api_client.update_file(
|
|
164
160
|
project_key=self.project,
|
|
165
161
|
repository_slug=self.repository,
|
|
166
|
-
content=
|
|
167
|
-
message=f"Update {file_path}",
|
|
162
|
+
content=content,
|
|
163
|
+
message=commit_message or f"Update {file_path}",
|
|
168
164
|
branch=branch,
|
|
169
165
|
filename=file_path,
|
|
170
|
-
source_commit_id=source_commit['id']
|
|
166
|
+
source_commit_id=source_commit['id'],
|
|
171
167
|
)
|
|
172
168
|
|
|
173
169
|
def get_pull_request_commits(self, pr_id: str) -> List[Dict[str, Any]]:
|
|
@@ -294,7 +290,37 @@ class BitbucketCloudApi(BitbucketApiAbstract):
|
|
|
294
290
|
return None
|
|
295
291
|
|
|
296
292
|
def get_file(self, file_path: str, branch: str) -> str:
|
|
297
|
-
return
|
|
293
|
+
"""Fetch a file's content from Bitbucket Cloud and return it as text.
|
|
294
|
+
|
|
295
|
+
Uses the 'get' endpoint with advanced_mode to get a rich response object.
|
|
296
|
+
"""
|
|
297
|
+
try:
|
|
298
|
+
file_response = self.repository.get(
|
|
299
|
+
path=f"src/{branch}/{file_path}",
|
|
300
|
+
advanced_mode=True,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Prefer HTTP status when available
|
|
304
|
+
status = getattr(file_response, "status_code", None)
|
|
305
|
+
if status is not None and status != 200:
|
|
306
|
+
raise ToolException(
|
|
307
|
+
f"Failed to retrieve text from file '{file_path}' from branch '{branch}': "
|
|
308
|
+
f"HTTP {status}"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Safely extract text content
|
|
312
|
+
file_text = getattr(file_response, "text", None)
|
|
313
|
+
if not isinstance(file_text, str) or not file_text:
|
|
314
|
+
raise ToolException(
|
|
315
|
+
f"File '{file_path}' from branch '{branch}' is empty or could not be retrieved."
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
return file_text
|
|
319
|
+
except Exception as e:
|
|
320
|
+
# Network/transport or client-level failure
|
|
321
|
+
raise ToolException(
|
|
322
|
+
f"Failed to retrieve text from file '{file_path}' from branch '{branch}': {e}"
|
|
323
|
+
)
|
|
298
324
|
|
|
299
325
|
def get_files_list(self, file_path: str, branch: str) -> list:
|
|
300
326
|
files_list = []
|
|
@@ -315,22 +341,10 @@ class BitbucketCloudApi(BitbucketApiAbstract):
|
|
|
315
341
|
return self.repository.post(path='src', data=form_data, files={},
|
|
316
342
|
headers={'Content-Type': 'application/x-www-form-urlencoded'})
|
|
317
343
|
|
|
318
|
-
def
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
for old, new in parse_old_new_markers(file_query=update_query):
|
|
323
|
-
if not old.strip():
|
|
324
|
-
continue
|
|
325
|
-
updated_file_content = updated_file_content.replace(old, new)
|
|
326
|
-
|
|
327
|
-
if file_content == updated_file_content:
|
|
328
|
-
return ToolException(
|
|
329
|
-
"File content was not updated because old content was not found or empty. "
|
|
330
|
-
"It may be helpful to use the read_file action to get "
|
|
331
|
-
"the current file contents."
|
|
332
|
-
)
|
|
333
|
-
return self.create_file(file_path, updated_file_content, branch)
|
|
344
|
+
def _write_file(self, file_path: str, content: str, branch: str, commit_message: str) -> str:
|
|
345
|
+
"""Write updated file content to Bitbucket Cloud.
|
|
346
|
+
"""
|
|
347
|
+
return self.create_file(file_path=file_path, file_contents=content, branch=branch)
|
|
334
348
|
|
|
335
349
|
def get_pull_request_commits(self, pr_id: str) -> List[Dict[str, Any]]:
|
|
336
350
|
"""
|
|
@@ -620,11 +620,18 @@ class ConfluenceAPIWrapper(NonCodeIndexerToolkit):
|
|
|
620
620
|
def _process_search(self, cql, skip_images: bool = False):
|
|
621
621
|
start = 0
|
|
622
622
|
pages_info = []
|
|
623
|
+
seen_ids: set = set() # Track seen page IDs to avoid duplicates
|
|
623
624
|
for _ in range((self.max_pages + self.limit - 1) // self.limit):
|
|
624
625
|
pages = self.client.cql(cql, start=start, limit=self.limit).get("results", [])
|
|
625
626
|
if not pages:
|
|
626
627
|
break
|
|
627
|
-
|
|
628
|
+
# Deduplicate page IDs before processing
|
|
629
|
+
page_ids = []
|
|
630
|
+
for page in pages:
|
|
631
|
+
page_id = page['content']['id']
|
|
632
|
+
if page_id not in seen_ids:
|
|
633
|
+
seen_ids.add(page_id)
|
|
634
|
+
page_ids.append(page_id)
|
|
628
635
|
for page in self.get_pages_by_id(page_ids, skip_images):
|
|
629
636
|
page_info = {
|
|
630
637
|
'content': page.page_content,
|
alita_sdk/tools/elitea_base.py
CHANGED
|
@@ -837,10 +837,7 @@ class BaseCodeToolApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
837
837
|
commit_message: Commit message (VCS toolkits only)
|
|
838
838
|
|
|
839
839
|
Returns:
|
|
840
|
-
Success message or
|
|
841
|
-
|
|
842
|
-
Raises:
|
|
843
|
-
ToolException: If file is not text-editable or edit fails
|
|
840
|
+
Success message or raises ToolException on failure.
|
|
844
841
|
"""
|
|
845
842
|
from .utils.text_operations import parse_old_new_markers, is_text_editable, try_apply_edit
|
|
846
843
|
from langchain_core.callbacks import dispatch_custom_event
|
|
@@ -868,45 +865,35 @@ class BaseCodeToolApiWrapper(BaseVectorStoreToolApiWrapper):
|
|
|
868
865
|
raise current_content if isinstance(current_content, Exception) else ToolException(str(current_content))
|
|
869
866
|
except Exception as e:
|
|
870
867
|
raise ToolException(f"Failed to read file {file_path}: {e}")
|
|
871
|
-
|
|
872
|
-
# Apply all edits (
|
|
868
|
+
|
|
869
|
+
# Apply all edits (stop on first warning/error)
|
|
873
870
|
updated_content = current_content
|
|
874
|
-
fallbacks_used = 0
|
|
875
871
|
edits_applied = 0
|
|
876
872
|
for old_text, new_text in edits:
|
|
877
873
|
if not old_text.strip():
|
|
878
874
|
continue
|
|
879
875
|
|
|
880
|
-
new_updated,
|
|
876
|
+
new_updated, error_message = try_apply_edit(
|
|
881
877
|
content=updated_content,
|
|
882
878
|
old_text=old_text,
|
|
883
879
|
new_text=new_text,
|
|
884
880
|
file_path=file_path,
|
|
885
881
|
)
|
|
886
882
|
|
|
887
|
-
if
|
|
888
|
-
|
|
889
|
-
logger.warning(
|
|
890
|
-
"Old content not found or could not be safely matched in %s. Snippet: %s...",
|
|
891
|
-
file_path,
|
|
892
|
-
old_text[:100].replace("\n", "\\n"),
|
|
893
|
-
)
|
|
894
|
-
continue
|
|
883
|
+
if error_message:
|
|
884
|
+
return error_message
|
|
895
885
|
|
|
896
886
|
# A replacement was applied
|
|
897
887
|
edits_applied += 1
|
|
898
|
-
if used_fallback:
|
|
899
|
-
fallbacks_used += 1
|
|
900
|
-
|
|
901
888
|
updated_content = new_updated
|
|
902
889
|
|
|
903
890
|
# Check if any changes were made
|
|
904
|
-
if current_content == updated_content
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
891
|
+
if current_content == updated_content:
|
|
892
|
+
# At least one edit was applied, but the final content is identical.
|
|
893
|
+
# This usually means the sequence of OLD/NEW pairs is redundant or cancels out.
|
|
894
|
+
return (f"Edits for {file_path} were applied but the final content is identical to the original. "
|
|
895
|
+
"The sequence of OLD/NEW pairs appears to be redundant or self-cancelling. "
|
|
896
|
+
"Please simplify or review the update_query.")
|
|
910
897
|
|
|
911
898
|
# Write updated content
|
|
912
899
|
try:
|
|
@@ -1116,60 +1103,77 @@ def extend_with_file_operations(method):
|
|
|
1116
1103
|
"""
|
|
1117
1104
|
Decorator to automatically add file operation tools to toolkits that implement
|
|
1118
1105
|
_read_file and _write_file methods.
|
|
1119
|
-
|
|
1106
|
+
|
|
1120
1107
|
Adds:
|
|
1121
1108
|
- read_file_chunk: Read specific line ranges
|
|
1122
1109
|
- read_multiple_files: Batch read files
|
|
1123
1110
|
- search_file: Search for patterns in files
|
|
1124
1111
|
- edit_file: Edit files using OLD/NEW markers
|
|
1112
|
+
|
|
1113
|
+
Custom Schema Support:
|
|
1114
|
+
Toolkits can provide custom schemas by implementing _get_file_operation_schemas() method
|
|
1115
|
+
that returns a dict mapping tool names to Pydantic models. This allows toolkits like
|
|
1116
|
+
ArtifactWrapper to use bucket_name instead of branch.
|
|
1117
|
+
|
|
1118
|
+
Example:
|
|
1119
|
+
def _get_file_operation_schemas(self):
|
|
1120
|
+
return {
|
|
1121
|
+
"read_file_chunk": MyCustomReadFileChunkInput,
|
|
1122
|
+
"read_multiple_files": MyCustomReadMultipleFilesInput,
|
|
1123
|
+
}
|
|
1125
1124
|
"""
|
|
1126
1125
|
def wrapper(self, *args, **kwargs):
|
|
1127
1126
|
tools = method(self, *args, **kwargs)
|
|
1128
|
-
|
|
1127
|
+
|
|
1129
1128
|
# Only add file operations if toolkit has implemented the required methods
|
|
1130
1129
|
# Check for both _read_file and _write_file methods
|
|
1131
1130
|
has_file_ops = (hasattr(self, '_read_file') and callable(getattr(self, '_read_file')) and
|
|
1132
1131
|
hasattr(self, '_write_file') and callable(getattr(self, '_write_file')))
|
|
1133
|
-
|
|
1132
|
+
|
|
1134
1133
|
if has_file_ops:
|
|
1135
1134
|
# Import schemas from elitea_base
|
|
1136
1135
|
from . import elitea_base
|
|
1137
|
-
|
|
1136
|
+
|
|
1137
|
+
# Check for toolkit-specific custom schemas
|
|
1138
|
+
custom_schemas = {}
|
|
1139
|
+
if hasattr(self, '_get_file_operation_schemas') and callable(getattr(self, '_get_file_operation_schemas')):
|
|
1140
|
+
custom_schemas = self._get_file_operation_schemas() or {}
|
|
1141
|
+
|
|
1138
1142
|
file_operation_tools = [
|
|
1139
1143
|
{
|
|
1140
1144
|
"name": "read_file_chunk",
|
|
1141
1145
|
"mode": "read_file_chunk",
|
|
1142
1146
|
"ref": self.read_file_chunk,
|
|
1143
1147
|
"description": self.read_file_chunk.__doc__,
|
|
1144
|
-
"args_schema": elitea_base.ReadFileChunkInput
|
|
1148
|
+
"args_schema": custom_schemas.get("read_file_chunk", elitea_base.ReadFileChunkInput)
|
|
1145
1149
|
},
|
|
1146
1150
|
{
|
|
1147
1151
|
"name": "read_multiple_files",
|
|
1148
1152
|
"mode": "read_multiple_files",
|
|
1149
1153
|
"ref": self.read_multiple_files,
|
|
1150
1154
|
"description": self.read_multiple_files.__doc__,
|
|
1151
|
-
"args_schema": elitea_base.ReadMultipleFilesInput
|
|
1155
|
+
"args_schema": custom_schemas.get("read_multiple_files", elitea_base.ReadMultipleFilesInput)
|
|
1152
1156
|
},
|
|
1153
1157
|
{
|
|
1154
1158
|
"name": "search_file",
|
|
1155
1159
|
"mode": "search_file",
|
|
1156
1160
|
"ref": self.search_file,
|
|
1157
1161
|
"description": self.search_file.__doc__,
|
|
1158
|
-
"args_schema": elitea_base.SearchFileInput
|
|
1162
|
+
"args_schema": custom_schemas.get("search_file", elitea_base.SearchFileInput)
|
|
1159
1163
|
},
|
|
1160
1164
|
{
|
|
1161
1165
|
"name": "edit_file",
|
|
1162
1166
|
"mode": "edit_file",
|
|
1163
1167
|
"ref": self.edit_file,
|
|
1164
1168
|
"description": self.edit_file.__doc__,
|
|
1165
|
-
"args_schema": elitea_base.EditFileInput
|
|
1169
|
+
"args_schema": custom_schemas.get("edit_file", elitea_base.EditFileInput)
|
|
1166
1170
|
},
|
|
1167
1171
|
]
|
|
1168
|
-
|
|
1172
|
+
|
|
1169
1173
|
tools.extend(file_operation_tools)
|
|
1170
|
-
|
|
1174
|
+
|
|
1171
1175
|
return tools
|
|
1172
|
-
|
|
1176
|
+
|
|
1173
1177
|
return wrapper
|
|
1174
1178
|
|
|
1175
1179
|
|