vectara-agentic 0.2.13__py3-none-any.whl → 0.2.14__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 vectara-agentic might be problematic. Click here for more details.
- tests/test_groq.py +120 -0
- tests/test_tools.py +41 -5
- tests/test_vectara_llms.py +0 -11
- vectara_agentic/_version.py +1 -1
- vectara_agentic/agent.py +65 -1
- vectara_agentic/llm_utils.py +174 -0
- vectara_agentic/tool_utils.py +513 -0
- vectara_agentic/tools.py +23 -471
- vectara_agentic/tools_catalog.py +2 -1
- vectara_agentic/utils.py +0 -153
- {vectara_agentic-0.2.13.dist-info → vectara_agentic-0.2.14.dist-info}/METADATA +25 -11
- {vectara_agentic-0.2.13.dist-info → vectara_agentic-0.2.14.dist-info}/RECORD +15 -12
- {vectara_agentic-0.2.13.dist-info → vectara_agentic-0.2.14.dist-info}/WHEEL +1 -1
- {vectara_agentic-0.2.13.dist-info → vectara_agentic-0.2.14.dist-info}/licenses/LICENSE +0 -0
- {vectara_agentic-0.2.13.dist-info → vectara_agentic-0.2.14.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the ToolsFactory class for creating agent tools.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from typing import (
|
|
9
|
+
Callable, List, Dict, Any, Optional, Union, Type, Tuple,
|
|
10
|
+
Sequence
|
|
11
|
+
)
|
|
12
|
+
from pydantic import BaseModel, create_model
|
|
13
|
+
from pydantic_core import PydanticUndefined
|
|
14
|
+
|
|
15
|
+
from llama_index.core.tools import FunctionTool
|
|
16
|
+
from llama_index.core.tools.function_tool import AsyncCallable
|
|
17
|
+
from llama_index.core.tools.types import ToolMetadata, ToolOutput
|
|
18
|
+
from llama_index.core.workflow.context import Context
|
|
19
|
+
|
|
20
|
+
from llama_index.core.tools.types import BaseTool
|
|
21
|
+
from llama_index.core.base.llms.types import ChatMessage, MessageRole
|
|
22
|
+
from llama_index.llms.openai.utils import resolve_tool_choice
|
|
23
|
+
|
|
24
|
+
from .types import ToolType
|
|
25
|
+
from .utils import is_float
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _updated_openai_prepare_chat_with_tools(
|
|
29
|
+
self,
|
|
30
|
+
tools: Sequence["BaseTool"],
|
|
31
|
+
user_msg: Optional[Union[str, ChatMessage]] = None,
|
|
32
|
+
chat_history: Optional[List[ChatMessage]] = None,
|
|
33
|
+
verbose: bool = False,
|
|
34
|
+
allow_parallel_tool_calls: bool = False,
|
|
35
|
+
tool_choice: Union[str, dict] = "auto",
|
|
36
|
+
strict: Optional[bool] = None,
|
|
37
|
+
**kwargs: Any,
|
|
38
|
+
) -> Dict[str, Any]:
|
|
39
|
+
"""Predict and call the tool."""
|
|
40
|
+
tool_specs = [tool.metadata.to_openai_tool(skip_length_check=True) for tool in tools]
|
|
41
|
+
|
|
42
|
+
# if strict is passed in, use, else default to the class-level attribute, else default to True`
|
|
43
|
+
strict = strict if strict is not None else self.strict
|
|
44
|
+
|
|
45
|
+
if self.metadata.is_function_calling_model:
|
|
46
|
+
for tool_spec in tool_specs:
|
|
47
|
+
if tool_spec["type"] == "function":
|
|
48
|
+
tool_spec["function"]["strict"] = strict
|
|
49
|
+
# in current openai 1.40.0 it is always false.
|
|
50
|
+
tool_spec["function"]["parameters"]["additionalProperties"] = False
|
|
51
|
+
|
|
52
|
+
if isinstance(user_msg, str):
|
|
53
|
+
user_msg = ChatMessage(role=MessageRole.USER, content=user_msg)
|
|
54
|
+
|
|
55
|
+
messages = chat_history or []
|
|
56
|
+
if user_msg:
|
|
57
|
+
messages.append(user_msg)
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
"messages": messages,
|
|
61
|
+
"tools": tool_specs or None,
|
|
62
|
+
"tool_choice": resolve_tool_choice(tool_choice) if tool_specs else None,
|
|
63
|
+
**kwargs,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class VectaraToolMetadata(ToolMetadata):
|
|
67
|
+
"""
|
|
68
|
+
A subclass of ToolMetadata adding the tool_type attribute.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
tool_type: ToolType
|
|
72
|
+
|
|
73
|
+
def __init__(self, tool_type: ToolType, **kwargs):
|
|
74
|
+
super().__init__(**kwargs)
|
|
75
|
+
self.tool_type = tool_type
|
|
76
|
+
|
|
77
|
+
def __repr__(self) -> str:
|
|
78
|
+
"""
|
|
79
|
+
Returns a string representation of the VectaraToolMetadata object, including the tool_type attribute.
|
|
80
|
+
"""
|
|
81
|
+
base_repr = super().__repr__()
|
|
82
|
+
return f"{base_repr}, tool_type={self.tool_type}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class VectaraTool(FunctionTool):
|
|
86
|
+
"""
|
|
87
|
+
A subclass of FunctionTool adding the tool_type attribute.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
tool_type: ToolType,
|
|
93
|
+
metadata: ToolMetadata,
|
|
94
|
+
fn: Optional[Callable[..., Any]] = None,
|
|
95
|
+
async_fn: Optional[AsyncCallable] = None,
|
|
96
|
+
) -> None:
|
|
97
|
+
metadata_dict = (
|
|
98
|
+
metadata.dict() if hasattr(metadata, "dict") else metadata.__dict__
|
|
99
|
+
)
|
|
100
|
+
vm = VectaraToolMetadata(tool_type=tool_type, **metadata_dict)
|
|
101
|
+
super().__init__(fn, vm, async_fn)
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def from_defaults(
|
|
105
|
+
cls,
|
|
106
|
+
fn: Optional[Callable[..., Any]] = None,
|
|
107
|
+
name: Optional[str] = None,
|
|
108
|
+
description: Optional[str] = None,
|
|
109
|
+
return_direct: bool = False,
|
|
110
|
+
fn_schema: Optional[Type[BaseModel]] = None,
|
|
111
|
+
async_fn: Optional[AsyncCallable] = None,
|
|
112
|
+
tool_metadata: Optional[ToolMetadata] = None,
|
|
113
|
+
callback: Optional[Callable[[Any], Any]] = None,
|
|
114
|
+
async_callback: Optional[AsyncCallable] = None,
|
|
115
|
+
tool_type: ToolType = ToolType.QUERY,
|
|
116
|
+
) -> "VectaraTool":
|
|
117
|
+
tool = FunctionTool.from_defaults(
|
|
118
|
+
fn,
|
|
119
|
+
name,
|
|
120
|
+
description,
|
|
121
|
+
return_direct,
|
|
122
|
+
fn_schema,
|
|
123
|
+
async_fn,
|
|
124
|
+
tool_metadata,
|
|
125
|
+
callback,
|
|
126
|
+
async_callback,
|
|
127
|
+
)
|
|
128
|
+
vectara_tool = cls(
|
|
129
|
+
tool_type=tool_type,
|
|
130
|
+
fn=tool.fn,
|
|
131
|
+
metadata=tool.metadata,
|
|
132
|
+
async_fn=tool.async_fn,
|
|
133
|
+
)
|
|
134
|
+
return vectara_tool
|
|
135
|
+
|
|
136
|
+
def __str__(self) -> str:
|
|
137
|
+
return f"Tool(name={self.metadata.name}, " f"Tool metadata={self.metadata})"
|
|
138
|
+
|
|
139
|
+
def __repr__(self) -> str:
|
|
140
|
+
return str(self)
|
|
141
|
+
|
|
142
|
+
def __eq__(self, other):
|
|
143
|
+
if not isinstance(other, VectaraTool):
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
if self.metadata.tool_type != other.metadata.tool_type:
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
if self.metadata.name != other.metadata.name:
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
# If schema is a dict-like object, compare the dict representation
|
|
153
|
+
try:
|
|
154
|
+
# Try to get schema as dict if possible
|
|
155
|
+
if hasattr(self.metadata.fn_schema, "schema"):
|
|
156
|
+
self_schema = self.metadata.fn_schema.schema
|
|
157
|
+
other_schema = other.metadata.fn_schema.schema
|
|
158
|
+
|
|
159
|
+
# Compare only properties and required fields
|
|
160
|
+
self_props = self_schema.get("properties", {})
|
|
161
|
+
other_props = other_schema.get("properties", {})
|
|
162
|
+
|
|
163
|
+
self_required = self_schema.get("required", [])
|
|
164
|
+
other_required = other_schema.get("required", [])
|
|
165
|
+
|
|
166
|
+
return self_props.keys() == other_props.keys() and set(
|
|
167
|
+
self_required
|
|
168
|
+
) == set(other_required)
|
|
169
|
+
except Exception:
|
|
170
|
+
# If any exception occurs during schema comparison, fall back to name comparison
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
def call(
|
|
176
|
+
self, *args: Any, ctx: Optional[Context] = None, **kwargs: Any
|
|
177
|
+
) -> ToolOutput:
|
|
178
|
+
try:
|
|
179
|
+
return super().call(*args, ctx=ctx, **kwargs)
|
|
180
|
+
except TypeError as e:
|
|
181
|
+
sig = inspect.signature(self.metadata.fn_schema)
|
|
182
|
+
valid_parameters = list(sig.parameters.keys())
|
|
183
|
+
params_str = ", ".join(valid_parameters)
|
|
184
|
+
|
|
185
|
+
err_output = ToolOutput(
|
|
186
|
+
tool_name=self.metadata.name,
|
|
187
|
+
content=(
|
|
188
|
+
f"Wrong argument used when calling {self.metadata.name}: {str(e)}. "
|
|
189
|
+
f"Valid arguments: {params_str}. please call the tool again with the correct arguments."
|
|
190
|
+
),
|
|
191
|
+
raw_input={"args": args, "kwargs": kwargs},
|
|
192
|
+
raw_output={"response": str(e)},
|
|
193
|
+
)
|
|
194
|
+
return err_output
|
|
195
|
+
except Exception as e:
|
|
196
|
+
err_output = ToolOutput(
|
|
197
|
+
tool_name=self.metadata.name,
|
|
198
|
+
content=f"Tool {self.metadata.name} Malfunction: {str(e)}",
|
|
199
|
+
raw_input={"args": args, "kwargs": kwargs},
|
|
200
|
+
raw_output={"response": str(e)},
|
|
201
|
+
)
|
|
202
|
+
return err_output
|
|
203
|
+
|
|
204
|
+
async def acall(
|
|
205
|
+
self, *args: Any, ctx: Optional[Context] = None, **kwargs: Any
|
|
206
|
+
) -> ToolOutput:
|
|
207
|
+
try:
|
|
208
|
+
return await super().acall(*args, ctx=ctx, **kwargs)
|
|
209
|
+
except TypeError as e:
|
|
210
|
+
sig = inspect.signature(self.metadata.fn_schema)
|
|
211
|
+
valid_parameters = list(sig.parameters.keys())
|
|
212
|
+
params_str = ", ".join(valid_parameters)
|
|
213
|
+
|
|
214
|
+
err_output = ToolOutput(
|
|
215
|
+
tool_name=self.metadata.name,
|
|
216
|
+
content=(
|
|
217
|
+
f"Wrong argument used when calling {self.metadata.name}: {str(e)}. "
|
|
218
|
+
f"Valid arguments: {params_str}. please call the tool again with the correct arguments."
|
|
219
|
+
),
|
|
220
|
+
raw_input={"args": args, "kwargs": kwargs},
|
|
221
|
+
raw_output={"response": str(e)},
|
|
222
|
+
)
|
|
223
|
+
return err_output
|
|
224
|
+
except Exception as e:
|
|
225
|
+
err_output = ToolOutput(
|
|
226
|
+
tool_name=self.metadata.name,
|
|
227
|
+
content=f"Tool {self.metadata.name} Malfunction: {str(e)}",
|
|
228
|
+
raw_input={"args": args, "kwargs": kwargs},
|
|
229
|
+
raw_output={"response": str(e)},
|
|
230
|
+
)
|
|
231
|
+
return err_output
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _create_tool_from_dynamic_function(
|
|
235
|
+
function: Callable[..., ToolOutput],
|
|
236
|
+
tool_name: str,
|
|
237
|
+
tool_description: str,
|
|
238
|
+
base_params_model: Type[BaseModel],
|
|
239
|
+
tool_args_schema: Type[BaseModel],
|
|
240
|
+
compact_docstring: bool = False,
|
|
241
|
+
) -> VectaraTool:
|
|
242
|
+
base_params = []
|
|
243
|
+
|
|
244
|
+
if tool_args_schema is None:
|
|
245
|
+
|
|
246
|
+
class EmptyBaseModel(BaseModel):
|
|
247
|
+
"""empty base model"""
|
|
248
|
+
|
|
249
|
+
tool_args_schema = EmptyBaseModel
|
|
250
|
+
|
|
251
|
+
fields = {}
|
|
252
|
+
for param_name, model_field in base_params_model.model_fields.items():
|
|
253
|
+
field_type = base_params_model.__annotations__.get(
|
|
254
|
+
param_name, str
|
|
255
|
+
)
|
|
256
|
+
default_value = (
|
|
257
|
+
model_field.default
|
|
258
|
+
if model_field.default is not None
|
|
259
|
+
else inspect.Parameter.empty
|
|
260
|
+
)
|
|
261
|
+
param = inspect.Parameter(
|
|
262
|
+
param_name,
|
|
263
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
264
|
+
default=default_value,
|
|
265
|
+
annotation=field_type,
|
|
266
|
+
)
|
|
267
|
+
base_params.append(param)
|
|
268
|
+
fields[param_name] = (
|
|
269
|
+
field_type,
|
|
270
|
+
model_field.default if model_field.default is not None else ...,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# Add tool_args_schema fields to the fields dict if not already included.
|
|
274
|
+
# Also add them to the function signature by creating new inspect.Parameter objects.
|
|
275
|
+
for field_name, field_info in tool_args_schema.model_fields.items():
|
|
276
|
+
if field_name in fields:
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
default_value = field_info.default if field_info.default is not None else ...
|
|
280
|
+
field_type = tool_args_schema.__annotations__.get(field_name, None)
|
|
281
|
+
fields[field_name] = (field_type, default_value)
|
|
282
|
+
param = inspect.Parameter(
|
|
283
|
+
field_name,
|
|
284
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
285
|
+
default=(
|
|
286
|
+
default_value
|
|
287
|
+
if default_value is not ...
|
|
288
|
+
else inspect.Parameter.empty
|
|
289
|
+
),
|
|
290
|
+
annotation=field_type,
|
|
291
|
+
)
|
|
292
|
+
base_params.append(param)
|
|
293
|
+
|
|
294
|
+
# Create the dynamic schema with both base_params_model and tool_args_schema fields.
|
|
295
|
+
fn_schema = create_model(f"{tool_name}_schema", **fields)
|
|
296
|
+
|
|
297
|
+
# Combine parameters into a function signature.
|
|
298
|
+
all_params = base_params[:] # Now all_params contains parameters from both models.
|
|
299
|
+
required_params = [p for p in all_params if p.default is inspect.Parameter.empty]
|
|
300
|
+
optional_params = [
|
|
301
|
+
p for p in all_params if p.default is not inspect.Parameter.empty
|
|
302
|
+
]
|
|
303
|
+
function.__signature__ = inspect.Signature(required_params + optional_params)
|
|
304
|
+
function.__annotations__["return"] = dict[str, Any]
|
|
305
|
+
function.__name__ = re.sub(r"[^A-Za-z0-9_]", "_", tool_name)
|
|
306
|
+
|
|
307
|
+
# Build a docstring using parameter descriptions from the BaseModels.
|
|
308
|
+
params_str = ", ".join(
|
|
309
|
+
f"{p.name}: {p.annotation.__name__ if hasattr(p.annotation, '__name__') else p.annotation}"
|
|
310
|
+
for p in all_params
|
|
311
|
+
)
|
|
312
|
+
signature_line = f"{tool_name}({params_str}) -> dict[str, Any]"
|
|
313
|
+
if compact_docstring:
|
|
314
|
+
doc_lines = [
|
|
315
|
+
tool_description.strip(),
|
|
316
|
+
]
|
|
317
|
+
else:
|
|
318
|
+
doc_lines = [
|
|
319
|
+
signature_line,
|
|
320
|
+
"",
|
|
321
|
+
tool_description.strip(),
|
|
322
|
+
]
|
|
323
|
+
doc_lines += [
|
|
324
|
+
"",
|
|
325
|
+
"Args:",
|
|
326
|
+
]
|
|
327
|
+
for param in all_params:
|
|
328
|
+
description = ""
|
|
329
|
+
if param.name in base_params_model.model_fields:
|
|
330
|
+
description = base_params_model.model_fields[param.name].description
|
|
331
|
+
elif param.name in tool_args_schema.model_fields:
|
|
332
|
+
description = tool_args_schema.model_fields[param.name].description
|
|
333
|
+
if not description:
|
|
334
|
+
description = ""
|
|
335
|
+
type_name = (
|
|
336
|
+
param.annotation.__name__
|
|
337
|
+
if hasattr(param.annotation, "__name__")
|
|
338
|
+
else str(param.annotation)
|
|
339
|
+
)
|
|
340
|
+
if (
|
|
341
|
+
param.default is not inspect.Parameter.empty
|
|
342
|
+
and param.default is not PydanticUndefined
|
|
343
|
+
):
|
|
344
|
+
default_text = f", default={param.default!r}"
|
|
345
|
+
else:
|
|
346
|
+
default_text = ""
|
|
347
|
+
doc_lines.append(f" - {param.name} ({type_name}){default_text}: {description}")
|
|
348
|
+
doc_lines.append("")
|
|
349
|
+
doc_lines.append("Returns:")
|
|
350
|
+
return_desc = getattr(
|
|
351
|
+
function, "__return_description__", "A dictionary containing the result data."
|
|
352
|
+
)
|
|
353
|
+
doc_lines.append(f" dict[str, Any]: {return_desc}")
|
|
354
|
+
|
|
355
|
+
initial_docstring = "\n".join(doc_lines)
|
|
356
|
+
collapsed_spaces = re.sub(r' {2,}', ' ', initial_docstring)
|
|
357
|
+
final_docstring = re.sub(r'\n{2,}', '\n', collapsed_spaces).strip()
|
|
358
|
+
function.__doc__ = final_docstring
|
|
359
|
+
|
|
360
|
+
tool = VectaraTool.from_defaults(
|
|
361
|
+
fn=function,
|
|
362
|
+
name=tool_name,
|
|
363
|
+
description=function.__doc__,
|
|
364
|
+
fn_schema=fn_schema,
|
|
365
|
+
tool_type=ToolType.QUERY,
|
|
366
|
+
)
|
|
367
|
+
return tool
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
Range = Tuple[float, float, bool, bool] # (min, max, min_inclusive, max_inclusive)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _parse_range(val_str: str) -> Range:
|
|
374
|
+
"""
|
|
375
|
+
Parses '[1,10)' or '(0.5, 5]' etc.
|
|
376
|
+
Returns (start, end, start_incl, end_incl) or raises ValueError.
|
|
377
|
+
"""
|
|
378
|
+
m = re.match(
|
|
379
|
+
r"""
|
|
380
|
+
^([\[\(])\s* # opening bracket
|
|
381
|
+
([+-]?\d+(\.\d*)?)\s*, # first number
|
|
382
|
+
\s*([+-]?\d+(\.\d*)?) # second number
|
|
383
|
+
\s*([\]\)])$ # closing bracket
|
|
384
|
+
""",
|
|
385
|
+
val_str,
|
|
386
|
+
re.VERBOSE,
|
|
387
|
+
)
|
|
388
|
+
if not m:
|
|
389
|
+
raise ValueError(f"Invalid range syntax: {val_str!r}")
|
|
390
|
+
start_inc = m.group(1) == "["
|
|
391
|
+
end_inc = m.group(7) == "]"
|
|
392
|
+
start = float(m.group(2))
|
|
393
|
+
end = float(m.group(4))
|
|
394
|
+
if start > end:
|
|
395
|
+
raise ValueError(f"Range lower bound greater than upper bound: {val_str!r}")
|
|
396
|
+
return start, end, start_inc, end_inc
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _parse_comparison(val_str: str) -> Tuple[str, Union[float, str, bool]]:
|
|
400
|
+
"""
|
|
401
|
+
Parses '>10', '<=3.14', '!=foo', \"='bar'\" etc.
|
|
402
|
+
Returns (operator, rhs) or raises ValueError.
|
|
403
|
+
"""
|
|
404
|
+
# pick off the operator
|
|
405
|
+
comparison_operators = [">=", "<=", "!=", ">", "<", "="]
|
|
406
|
+
numeric_only_operators = {">", "<", ">=", "<="}
|
|
407
|
+
for op in comparison_operators:
|
|
408
|
+
if val_str.startswith(op):
|
|
409
|
+
rhs = val_str[len(op) :].strip()
|
|
410
|
+
if op in numeric_only_operators:
|
|
411
|
+
try:
|
|
412
|
+
rhs_val = float(rhs)
|
|
413
|
+
except ValueError as e:
|
|
414
|
+
raise ValueError(
|
|
415
|
+
f"Numeric comparison {op!r} must have a number, got {rhs!r}"
|
|
416
|
+
) from e
|
|
417
|
+
return op, rhs_val
|
|
418
|
+
# = and != can be bool, numeric, or string
|
|
419
|
+
low = rhs.lower()
|
|
420
|
+
if low in ("true", "false"):
|
|
421
|
+
return op, (low == "true")
|
|
422
|
+
try:
|
|
423
|
+
return op, float(rhs)
|
|
424
|
+
except ValueError:
|
|
425
|
+
return op, rhs
|
|
426
|
+
raise ValueError(f"No valid comparison operator at start of {val_str!r}")
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _build_filter_string(
|
|
430
|
+
kwargs: Dict[str, Any], tool_args_type: Dict[str, dict], fixed_filter: str
|
|
431
|
+
) -> str:
|
|
432
|
+
"""
|
|
433
|
+
Build filter string for Vectara from kwargs
|
|
434
|
+
"""
|
|
435
|
+
filter_parts = []
|
|
436
|
+
for key, raw in kwargs.items():
|
|
437
|
+
if raw is None or raw == "":
|
|
438
|
+
continue
|
|
439
|
+
|
|
440
|
+
if raw is PydanticUndefined:
|
|
441
|
+
raise ValueError(
|
|
442
|
+
f"Value of argument {key!r} is undefined, and this is invalid. "
|
|
443
|
+
"Please form proper arguments and try again."
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
tool_args_dict = tool_args_type.get(key, {"type": "doc", "is_list": False})
|
|
447
|
+
prefix = tool_args_dict.get("type", "doc")
|
|
448
|
+
is_list = tool_args_dict.get("is_list", False)
|
|
449
|
+
|
|
450
|
+
if prefix not in ("doc", "part"):
|
|
451
|
+
raise ValueError(
|
|
452
|
+
f'Unrecognized prefix {prefix!r}. Please make sure to use either "doc" or "part" for the prefix.'
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# 1) native numeric
|
|
456
|
+
if isinstance(raw, (int, float)) or is_float(str(raw)):
|
|
457
|
+
val = str(raw)
|
|
458
|
+
if is_list:
|
|
459
|
+
filter_parts.append(f"({val} IN {prefix}.{key})")
|
|
460
|
+
else:
|
|
461
|
+
filter_parts.append(f"{prefix}.{key}={val}")
|
|
462
|
+
continue
|
|
463
|
+
|
|
464
|
+
# 2) native boolean
|
|
465
|
+
if isinstance(raw, bool):
|
|
466
|
+
val = "true" if raw else "false"
|
|
467
|
+
if is_list:
|
|
468
|
+
filter_parts.append(f"({val} IN {prefix}.{key})")
|
|
469
|
+
else:
|
|
470
|
+
filter_parts.append(f"{prefix}.{key}={val}")
|
|
471
|
+
continue
|
|
472
|
+
|
|
473
|
+
if not isinstance(raw, str):
|
|
474
|
+
raise ValueError(f"Unsupported type for {key!r}: {type(raw).__name__}")
|
|
475
|
+
|
|
476
|
+
val_str = raw.strip()
|
|
477
|
+
|
|
478
|
+
# 3) Range operator
|
|
479
|
+
if (val_str.startswith("[") or val_str.startswith("(")) and (
|
|
480
|
+
val_str.endswith("]") or val_str.endswith(")")
|
|
481
|
+
):
|
|
482
|
+
start, end, start_incl, end_incl = _parse_range(val_str)
|
|
483
|
+
conds = []
|
|
484
|
+
op1 = ">=" if start_incl else ">"
|
|
485
|
+
op2 = "<=" if end_incl else "<"
|
|
486
|
+
conds.append(f"{prefix}.{key} {op1} {start}")
|
|
487
|
+
conds.append(f"{prefix}.{key} {op2} {end}")
|
|
488
|
+
filter_parts.append("(" + " AND ".join(conds) + ")")
|
|
489
|
+
continue
|
|
490
|
+
|
|
491
|
+
# 4) comparison operator
|
|
492
|
+
try:
|
|
493
|
+
op, rhs = _parse_comparison(val_str)
|
|
494
|
+
except ValueError:
|
|
495
|
+
# no operator → treat as membership or equality-on-string
|
|
496
|
+
if is_list:
|
|
497
|
+
filter_parts.append(f"('{val_str}' IN {prefix}.{key})")
|
|
498
|
+
else:
|
|
499
|
+
filter_parts.append(f"{prefix}.{key}='{val_str}'")
|
|
500
|
+
else:
|
|
501
|
+
# normal comparison always binds to the field
|
|
502
|
+
if isinstance(rhs, bool):
|
|
503
|
+
rhs_sql = "true" if rhs else "false"
|
|
504
|
+
elif isinstance(rhs, (int, float)):
|
|
505
|
+
rhs_sql = str(rhs)
|
|
506
|
+
else:
|
|
507
|
+
rhs_sql = f"'{rhs}'"
|
|
508
|
+
filter_parts.append(f"{prefix}.{key}{op}{rhs_sql}")
|
|
509
|
+
|
|
510
|
+
joined = " AND ".join(filter_parts)
|
|
511
|
+
if fixed_filter and joined:
|
|
512
|
+
return f"({fixed_filter}) AND ({joined})"
|
|
513
|
+
return fixed_filter or joined
|