quantalogic 0.2.19__py3-none-any.whl → 0.2.20__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.
- quantalogic/agent.py +26 -5
- quantalogic/coding_agent.py +13 -0
- quantalogic/tools/__init__.py +3 -1
- quantalogic/tools/jinja_tool.py +91 -0
- quantalogic/tools/llm_tool.py +5 -0
- quantalogic/tools/search_definition_names.py +55 -4
- quantalogic/tools/tool.py +114 -7
- quantalogic/xml_tool_parser.py +4 -2
- quantalogic-0.2.20.dist-info/METADATA +515 -0
- {quantalogic-0.2.19.dist-info → quantalogic-0.2.20.dist-info}/RECORD +13 -12
- quantalogic-0.2.19.dist-info/METADATA +0 -992
- {quantalogic-0.2.19.dist-info → quantalogic-0.2.20.dist-info}/LICENSE +0 -0
- {quantalogic-0.2.19.dist-info → quantalogic-0.2.20.dist-info}/WHEEL +0 -0
- {quantalogic-0.2.19.dist-info → quantalogic-0.2.20.dist-info}/entry_points.txt +0 -0
quantalogic/agent.py
CHANGED
@@ -56,8 +56,8 @@ class Agent(BaseModel):
|
|
56
56
|
|
57
57
|
specific_expertise: str
|
58
58
|
model: GenerativeModel
|
59
|
-
memory: AgentMemory = AgentMemory()
|
60
|
-
variable_store: VariableMemory = VariableMemory()
|
59
|
+
memory: AgentMemory = AgentMemory() # A list User / Assistant Messages
|
60
|
+
variable_store: VariableMemory = VariableMemory() # A dictionary of variables (var1: value1, var2: value2)
|
61
61
|
tools: ToolManager = ToolManager()
|
62
62
|
event_emitter: EventEmitter = EventEmitter()
|
63
63
|
config: AgentConfig
|
@@ -78,13 +78,14 @@ class Agent(BaseModel):
|
|
78
78
|
self,
|
79
79
|
model_name: str = "",
|
80
80
|
memory: AgentMemory = AgentMemory(),
|
81
|
+
variable_store: VariableMemory = VariableMemory(),
|
81
82
|
tools: list[Tool] = [TaskCompleteTool()],
|
82
83
|
ask_for_user_validation: Callable[[str], bool] = console_ask_for_user_validation,
|
83
84
|
task_to_solve: str = "",
|
84
85
|
specific_expertise: str = "General AI assistant with coding and problem-solving capabilities",
|
85
86
|
get_environment: Callable[[], str] = get_environment,
|
86
|
-
compact_every_n_iterations: int | None = None, #
|
87
|
-
max_tokens_working_memory: int | None = None, #
|
87
|
+
compact_every_n_iterations: int | None = None, # if set the memory will be compacted every n iterations
|
88
|
+
max_tokens_working_memory: int | None = None, # if set the memory will be compacted each time the max_tokens_working_memory is reached
|
88
89
|
):
|
89
90
|
"""Initialize the agent with model, memory, tools, and configurations."""
|
90
91
|
try:
|
@@ -117,7 +118,7 @@ class Agent(BaseModel):
|
|
117
118
|
super().__init__(
|
118
119
|
model=GenerativeModel(model=model_name, event_emitter=event_emitter),
|
119
120
|
memory=memory,
|
120
|
-
variable_store=
|
121
|
+
variable_store=variable_store,
|
121
122
|
tools=tool_manager,
|
122
123
|
config=config,
|
123
124
|
ask_for_user_validation=ask_for_user_validation,
|
@@ -523,6 +524,13 @@ class Agent(BaseModel):
|
|
523
524
|
tuple containing:
|
524
525
|
- executed_tool name (str)
|
525
526
|
- tool execution response (Any)
|
527
|
+
|
528
|
+
Note:
|
529
|
+
Predefined variable properties take precedence over dynamically provided values.
|
530
|
+
This ensures consistent behavior when tools have both predefined properties
|
531
|
+
and runtime-provided arguments. The precedence order is:
|
532
|
+
1. Predefined properties from tool.get_injectable_properties_in_execution()
|
533
|
+
2. Runtime-provided arguments from arguments_with_values
|
526
534
|
"""
|
527
535
|
# Handle tool validation if required
|
528
536
|
if tool.need_validation:
|
@@ -560,6 +568,19 @@ class Agent(BaseModel):
|
|
560
568
|
arguments_with_values_interpolated = {
|
561
569
|
key: self._interpolate_variables(value) for key, value in arguments_with_values.items()
|
562
570
|
}
|
571
|
+
# test if tool need variables in context
|
572
|
+
if tool.need_variables:
|
573
|
+
# Inject variables into the tool if needed
|
574
|
+
arguments_with_values_interpolated["variables"] = self.variable_store
|
575
|
+
if tool.need_caller_context_memory:
|
576
|
+
# Inject caller context into the tool if needed
|
577
|
+
arguments_with_values_interpolated["caller_context_memory"] = self.memory.memory
|
578
|
+
|
579
|
+
# Add injectable variables
|
580
|
+
injectable_properties = tool.get_injectable_properties_in_execution()
|
581
|
+
for key, value in injectable_properties.items():
|
582
|
+
arguments_with_values_interpolated[key] = value
|
583
|
+
|
563
584
|
# Call tool execute with named arguments
|
564
585
|
response = tool.execute(**arguments_with_values_interpolated)
|
565
586
|
executed_tool = tool.name
|
quantalogic/coding_agent.py
CHANGED
@@ -5,6 +5,7 @@ from quantalogic.tools import (
|
|
5
5
|
EditWholeContentTool,
|
6
6
|
ExecuteBashCommandTool,
|
7
7
|
InputQuestionTool,
|
8
|
+
JinjaTool,
|
8
9
|
ListDirectoryTool,
|
9
10
|
LLMTool,
|
10
11
|
LLMVisionTool,
|
@@ -72,6 +73,7 @@ def create_coding_agent(
|
|
72
73
|
ExecuteBashCommandTool(),
|
73
74
|
InputQuestionTool(),
|
74
75
|
DuckDuckGoSearchTool(),
|
76
|
+
JinjaTool(),
|
75
77
|
]
|
76
78
|
|
77
79
|
if vision_model_name:
|
@@ -85,6 +87,9 @@ def create_coding_agent(
|
|
85
87
|
name="coding_consultant", # Handles implementation-level coding questions
|
86
88
|
on_token=console_print_token if not no_stream else None,
|
87
89
|
)
|
90
|
+
# Note: system_prompt is predefined in LLMTool properties and takes precedence over
|
91
|
+
# dynamically provided values. This ensures consistent behavior by prioritizing
|
92
|
+
# the predefined system prompt. See tool.py and agent.py for implementation details.
|
88
93
|
)
|
89
94
|
tools.append(
|
90
95
|
LLMTool(
|
@@ -94,6 +99,14 @@ def create_coding_agent(
|
|
94
99
|
on_token=console_print_token if not no_stream else None,
|
95
100
|
)
|
96
101
|
)
|
102
|
+
## Add a generic LLMTool
|
103
|
+
tools.append(
|
104
|
+
LLMTool(
|
105
|
+
model_name=model_name,
|
106
|
+
name="llm_tool",
|
107
|
+
on_token=console_print_token if not no_stream else None,
|
108
|
+
)
|
109
|
+
)
|
97
110
|
|
98
111
|
return Agent(
|
99
112
|
model_name=model_name,
|
quantalogic/tools/__init__.py
CHANGED
@@ -2,10 +2,12 @@
|
|
2
2
|
|
3
3
|
from .agent_tool import AgentTool
|
4
4
|
from .download_http_file_tool import DownloadHttpFileTool
|
5
|
+
from .duckduckgo_search_tool import DuckDuckGoSearchTool
|
5
6
|
from .edit_whole_content_tool import EditWholeContentTool
|
6
7
|
from .elixir_tool import ElixirTool
|
7
8
|
from .execute_bash_command_tool import ExecuteBashCommandTool
|
8
9
|
from .input_question_tool import InputQuestionTool
|
10
|
+
from .jinja_tool import JinjaTool
|
9
11
|
from .list_directory_tool import ListDirectoryTool
|
10
12
|
from .llm_tool import LLMTool
|
11
13
|
from .llm_vision_tool import LLMVisionTool
|
@@ -18,7 +20,6 @@ from .replace_in_file_tool import ReplaceInFileTool
|
|
18
20
|
from .ripgrep_tool import RipgrepTool
|
19
21
|
from .search_definition_names import SearchDefinitionNames
|
20
22
|
from .serpapi_search_tool import SerpApiSearchTool
|
21
|
-
from .duckduckgo_search_tool import DuckDuckGoSearchTool
|
22
23
|
from .task_complete_tool import TaskCompleteTool
|
23
24
|
from .tool import Tool, ToolArgument
|
24
25
|
from .unified_diff_tool import UnifiedDiffTool
|
@@ -51,4 +52,5 @@ __all__ = [
|
|
51
52
|
"MarkitdownTool",
|
52
53
|
"DownloadHttpFileTool",
|
53
54
|
"EditWholeContentTool",
|
55
|
+
"JinjaTool",
|
54
56
|
]
|
@@ -0,0 +1,91 @@
|
|
1
|
+
"""Tool for rendering Jinja templates with inline template string."""
|
2
|
+
|
3
|
+
from typing import Any, Dict, Optional
|
4
|
+
|
5
|
+
from jinja2 import Template
|
6
|
+
from loguru import logger
|
7
|
+
from pydantic import Field
|
8
|
+
|
9
|
+
from quantalogic.tools.tool import Tool, ToolArgument
|
10
|
+
|
11
|
+
|
12
|
+
class JinjaTool(Tool):
|
13
|
+
"""Tool for rendering Jinja templates with inline template string."""
|
14
|
+
|
15
|
+
name: str = "jinja_tool"
|
16
|
+
description: str = (
|
17
|
+
"Renders an inline Jinja2 template string with a predefined context.\n"
|
18
|
+
"You can use the variables in the template just as var1, var2, etc.\n"
|
19
|
+
"Useful for simple calculations or string operations.\n"
|
20
|
+
)
|
21
|
+
arguments: list = [
|
22
|
+
ToolArgument(
|
23
|
+
name="inline_template",
|
24
|
+
arg_type="string",
|
25
|
+
description="Inline Jinja2 template string to render. Has access to variables in the context.",
|
26
|
+
required=True,
|
27
|
+
example="Hello, {{ var1 }}! You have {{ var2|length }} items.",
|
28
|
+
),
|
29
|
+
]
|
30
|
+
need_variables: bool = True
|
31
|
+
|
32
|
+
# Add context as a field with a default empty dictionary
|
33
|
+
context: Dict[str, Any] = Field(default_factory=dict)
|
34
|
+
|
35
|
+
def __init__(self):
|
36
|
+
"""
|
37
|
+
Initialize JinjaTool with optional context.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
context (dict, optional): Context dictionary for template rendering.
|
41
|
+
"""
|
42
|
+
super().__init__()
|
43
|
+
|
44
|
+
def execute(self, inline_template: str,variables: Optional[Dict[str, Any]] = None) -> str:
|
45
|
+
"""
|
46
|
+
Render an inline Jinja2 template with the predefined context.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
inline_template (str): Inline template string to render.
|
50
|
+
variables (dict, optional): Additional variables to include in the context.
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
str: Rendered template content.
|
54
|
+
"""
|
55
|
+
try:
|
56
|
+
# Create Jinja2 template
|
57
|
+
template = Template(inline_template)
|
58
|
+
|
59
|
+
context = {}
|
60
|
+
if variables:
|
61
|
+
context.update(variables)
|
62
|
+
|
63
|
+
rendered_content = template.render(**context)
|
64
|
+
|
65
|
+
logger.info("Successfully rendered inline Jinja template")
|
66
|
+
return rendered_content
|
67
|
+
|
68
|
+
except Exception as e:
|
69
|
+
logger.error(f"Error rendering Jinja template: {e}")
|
70
|
+
raise
|
71
|
+
|
72
|
+
|
73
|
+
if __name__ == "__main__":
|
74
|
+
# Example of using JinjaTool with variables
|
75
|
+
tool = JinjaTool(context={
|
76
|
+
"var1": "World",
|
77
|
+
"var2": 42,
|
78
|
+
"var3": ["apple", "banana", "cherry"]
|
79
|
+
})
|
80
|
+
|
81
|
+
# Inline template demonstrating variable usage
|
82
|
+
template = "Hello {{ var1 }}! The answer is {{ var2 }}. Fruits: {% for fruit in var3 %}{{ fruit }}{% if not loop.last %}, {% endif %}{% endfor %}."
|
83
|
+
|
84
|
+
# Render the template
|
85
|
+
result = tool.execute(template)
|
86
|
+
print("Rendered Template:")
|
87
|
+
print(result)
|
88
|
+
|
89
|
+
# Print the tool's markdown representation
|
90
|
+
print("\nTool Markdown:")
|
91
|
+
print(tool.to_markdown())
|
quantalogic/tools/llm_tool.py
CHANGED
@@ -167,3 +167,8 @@ if __name__ == "__main__":
|
|
167
167
|
pirate_answer = pirate.execute(system_prompt=system_prompt, prompt=question, temperature=temperature)
|
168
168
|
print("\n")
|
169
169
|
print(f"Anwser: {pirate_answer}")
|
170
|
+
|
171
|
+
custom_tool = LLMTool(
|
172
|
+
model_name="openrouter/openai/gpt-4o-mini", system_prompt="You are a pirate.", on_token=console_print_token
|
173
|
+
)
|
174
|
+
print(custom_tool.to_markdown())
|
@@ -63,6 +63,20 @@ class SearchDefinitionNames(Tool):
|
|
63
63
|
required=False,
|
64
64
|
example="**/*.py",
|
65
65
|
),
|
66
|
+
ToolArgument(
|
67
|
+
name="page",
|
68
|
+
arg_type="int",
|
69
|
+
description="The page number to retrieve (1-based index).",
|
70
|
+
required=False,
|
71
|
+
example="1",
|
72
|
+
),
|
73
|
+
ToolArgument(
|
74
|
+
name="page_size",
|
75
|
+
arg_type="int",
|
76
|
+
description="The number of results per page (default: 10).",
|
77
|
+
required=False,
|
78
|
+
example="10",
|
79
|
+
),
|
66
80
|
]
|
67
81
|
|
68
82
|
def _get_language_handler(self, language_name: str):
|
@@ -89,7 +103,7 @@ class SearchDefinitionNames(Tool):
|
|
89
103
|
raise ValueError(f"Unsupported language: {language_name}")
|
90
104
|
|
91
105
|
def execute(
|
92
|
-
self, directory_path: str, language_name: str, file_pattern: str = "*", output_format: str = "text"
|
106
|
+
self, directory_path: str, language_name: str, file_pattern: str = "*", output_format: str = "text", page: int = 1, page_size: int = 10
|
93
107
|
) -> Union[str, Dict]:
|
94
108
|
"""Searches for definition names in a directory using Tree-sitter.
|
95
109
|
|
@@ -98,11 +112,19 @@ class SearchDefinitionNames(Tool):
|
|
98
112
|
language_name (str): The Tree-sitter language name.
|
99
113
|
file_pattern (str): Optional glob pattern to filter files (default: '*').
|
100
114
|
output_format (str): Output format ('text', 'json', 'markdown').
|
115
|
+
page (int): The page number to retrieve (1-based index).
|
116
|
+
page_size (int): The number of results per page (default: 10).
|
101
117
|
|
102
118
|
Returns:
|
103
119
|
Union[str, Dict]: The search results in the specified format.
|
104
120
|
"""
|
105
121
|
try:
|
122
|
+
# Validate pagination parameters
|
123
|
+
if page < 1:
|
124
|
+
raise ValueError("Page number must be a positive integer.")
|
125
|
+
if page_size < 1:
|
126
|
+
raise ValueError("Page size must be a positive integer.")
|
127
|
+
|
106
128
|
# Set up Tree-sitter based on language
|
107
129
|
language_handler = self._get_language_handler(language_name)
|
108
130
|
parser = Parser(language_handler.get_language())
|
@@ -140,7 +162,21 @@ class SearchDefinitionNames(Tool):
|
|
140
162
|
logger.warning(f"Error processing file {file_path}: {str(e)}")
|
141
163
|
continue
|
142
164
|
|
143
|
-
|
165
|
+
# Apply pagination
|
166
|
+
start_index = (page - 1) * page_size
|
167
|
+
end_index = start_index + page_size
|
168
|
+
paginated_results = results[start_index:end_index]
|
169
|
+
|
170
|
+
return self._format_results(
|
171
|
+
paginated_results,
|
172
|
+
directory_path,
|
173
|
+
language_name,
|
174
|
+
file_pattern,
|
175
|
+
output_format,
|
176
|
+
page,
|
177
|
+
page_size,
|
178
|
+
len(results)
|
179
|
+
)
|
144
180
|
|
145
181
|
except Exception as e:
|
146
182
|
logger.error(f"Error during search: {str(e)}")
|
@@ -263,6 +299,9 @@ class SearchDefinitionNames(Tool):
|
|
263
299
|
language_name: str,
|
264
300
|
file_pattern: str,
|
265
301
|
output_format: str = "text",
|
302
|
+
page: int = 1,
|
303
|
+
page_size: int = 10,
|
304
|
+
total_results: int = 0,
|
266
305
|
) -> Union[str, Dict]:
|
267
306
|
"""Formats the search results in the specified format.
|
268
307
|
|
@@ -272,6 +311,9 @@ class SearchDefinitionNames(Tool):
|
|
272
311
|
language_name: The language that was searched.
|
273
312
|
file_pattern: The file pattern that was used.
|
274
313
|
output_format: The desired output format.
|
314
|
+
page: The page number.
|
315
|
+
page_size: The number of results per page.
|
316
|
+
total_results: The total number of results.
|
275
317
|
|
276
318
|
Returns:
|
277
319
|
Union[str, Dict]: The formatted results.
|
@@ -281,6 +323,9 @@ class SearchDefinitionNames(Tool):
|
|
281
323
|
"directory": directory_path,
|
282
324
|
"language": language_name,
|
283
325
|
"file_pattern": file_pattern,
|
326
|
+
"page": page,
|
327
|
+
"page_size": page_size,
|
328
|
+
"total_results": total_results,
|
284
329
|
"results": [],
|
285
330
|
}
|
286
331
|
|
@@ -318,7 +363,10 @@ class SearchDefinitionNames(Tool):
|
|
318
363
|
markdown = "# Search Results\n\n"
|
319
364
|
markdown += f"- **Directory**: `{directory_path}`\n"
|
320
365
|
markdown += f"- **Language**: `{language_name}`\n"
|
321
|
-
markdown += f"- **File Pattern**: `{file_pattern}`\n
|
366
|
+
markdown += f"- **File Pattern**: `{file_pattern}`\n"
|
367
|
+
markdown += f"- **Page**: {page}\n"
|
368
|
+
markdown += f"- **Page Size**: {page_size}\n"
|
369
|
+
markdown += f"- **Total Results**: {total_results}\n\n"
|
322
370
|
|
323
371
|
for result in results:
|
324
372
|
markdown += f"## File: {result['file_path']}\n"
|
@@ -354,7 +402,10 @@ class SearchDefinitionNames(Tool):
|
|
354
402
|
text += "==============\n"
|
355
403
|
text += f"Directory: {directory_path}\n"
|
356
404
|
text += f"Language: {language_name}\n"
|
357
|
-
text += f"File Pattern: {file_pattern}\n
|
405
|
+
text += f"File Pattern: {file_pattern}\n"
|
406
|
+
text += f"Page: {page}\n"
|
407
|
+
text += f"Page Size: {page_size}\n"
|
408
|
+
text += f"Total Results: {total_results}\n\n"
|
358
409
|
|
359
410
|
for result in results:
|
360
411
|
text += f"File: {result['file_path']}\n"
|
quantalogic/tools/tool.py
CHANGED
@@ -6,6 +6,7 @@ with type-validated arguments and execution methods.
|
|
6
6
|
|
7
7
|
from typing import Any, Literal
|
8
8
|
|
9
|
+
from loguru import logger
|
9
10
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
10
11
|
|
11
12
|
|
@@ -27,9 +28,10 @@ class ToolArgument(BaseModel):
|
|
27
28
|
)
|
28
29
|
description: str | None = Field(default=None, description="A brief description of the argument.")
|
29
30
|
required: bool = Field(default=False, description="Indicates if the argument is required.")
|
30
|
-
default: str | None = Field(
|
31
|
+
default: str | None = Field(
|
32
|
+
default=None, description="The default value for the argument. This parameter is required."
|
33
|
+
)
|
31
34
|
example: str | None = Field(default=None, description="An example value to illustrate the argument's usage.")
|
32
|
-
need_validation: bool = Field(default=False, description="Indicates if the argument needs validation.")
|
33
35
|
|
34
36
|
|
35
37
|
class ToolDefinition(BaseModel):
|
@@ -42,12 +44,49 @@ class ToolDefinition(BaseModel):
|
|
42
44
|
need_validation: Flag to indicate if tool requires validation.
|
43
45
|
"""
|
44
46
|
|
45
|
-
model_config = ConfigDict(extra="
|
47
|
+
model_config = ConfigDict(extra="allow", validate_assignment=True)
|
46
48
|
|
47
49
|
name: str = Field(..., description="The unique name of the tool.")
|
48
50
|
description: str = Field(..., description="A brief description of what the tool does.")
|
49
51
|
arguments: list[ToolArgument] = Field(default_factory=list, description="A list of arguments the tool accepts.")
|
50
|
-
need_validation: bool = Field(
|
52
|
+
need_validation: bool = Field(
|
53
|
+
default=False,
|
54
|
+
description="When True, requires user confirmation before execution. Useful for tools that perform potentially destructive operations.",
|
55
|
+
)
|
56
|
+
need_variables: bool = Field(
|
57
|
+
default=False,
|
58
|
+
description="When True, provides access to the agent's variable store. Required for tools that need to interpolate variables (e.g., Jinja templates).",
|
59
|
+
)
|
60
|
+
need_caller_context_memory: bool = Field(
|
61
|
+
default=False,
|
62
|
+
description="When True, provides access to the agent's conversation history. Useful for tools that need context from previous interactions.",
|
63
|
+
)
|
64
|
+
|
65
|
+
def get_properties(self, exclude: list[str] | None = None) -> dict[str, Any]:
|
66
|
+
"""Return a dictionary of all non-None properties, excluding Tool class fields and specified fields.
|
67
|
+
|
68
|
+
Args:
|
69
|
+
exclude: Optional list of field names to exclude from the result
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
Dictionary of property names and values, excluding Tool class fields and specified fields.
|
73
|
+
"""
|
74
|
+
exclude = exclude or []
|
75
|
+
tool_fields = {
|
76
|
+
"name",
|
77
|
+
"description",
|
78
|
+
"arguments",
|
79
|
+
"need_validation",
|
80
|
+
"need_variables",
|
81
|
+
"need_caller_context_memory",
|
82
|
+
}
|
83
|
+
properties = {}
|
84
|
+
|
85
|
+
for name, value in self.__dict__.items():
|
86
|
+
if name not in tool_fields and name not in exclude and value is not None and not name.startswith("_"):
|
87
|
+
properties[name] = value
|
88
|
+
|
89
|
+
return properties
|
51
90
|
|
52
91
|
def to_json(self) -> str:
|
53
92
|
"""Convert the tool to a JSON string representation.
|
@@ -67,10 +106,21 @@ class ToolDefinition(BaseModel):
|
|
67
106
|
markdown = f"`{self.name}`:\n"
|
68
107
|
markdown += f"- **Description**: {self.description}\n\n"
|
69
108
|
|
109
|
+
properties_injectable = self.get_injectable_properties_in_execution()
|
110
|
+
|
70
111
|
# Parameters section
|
71
112
|
if self.arguments:
|
72
113
|
markdown += "- **Parameters**:\n"
|
114
|
+
parameters = ""
|
73
115
|
for arg in self.arguments:
|
116
|
+
# Skip if parameter name matches an object property with non-None value
|
117
|
+
# This enables automatic property injection during execution:
|
118
|
+
# When an object has a property matching an argument name,
|
119
|
+
# the agent will inject the property value at runtime,
|
120
|
+
# reducing manual input and improving flexibility
|
121
|
+
if properties_injectable.get(arg.name) is not None:
|
122
|
+
continue
|
123
|
+
|
74
124
|
required_status = "required" if arg.required else "optional"
|
75
125
|
# Prioritize example, then default, then create a generic description
|
76
126
|
value_info = ""
|
@@ -79,12 +129,15 @@ class ToolDefinition(BaseModel):
|
|
79
129
|
elif arg.default is not None:
|
80
130
|
value_info = f" (default: `{arg.default}`)"
|
81
131
|
|
82
|
-
|
132
|
+
parameters += (
|
83
133
|
f" - `{arg.name}`: "
|
84
134
|
f"({required_status}{value_info})\n"
|
85
|
-
f" {arg.description or '
|
135
|
+
f" {arg.description or ''}\n"
|
86
136
|
)
|
87
|
-
|
137
|
+
if len(parameters) > 0:
|
138
|
+
markdown += parameters + "\n\n"
|
139
|
+
else:
|
140
|
+
markdown += "None\n\n"
|
88
141
|
|
89
142
|
# Usage section with XML-style example
|
90
143
|
markdown += "**Usage**:\n"
|
@@ -93,6 +146,8 @@ class ToolDefinition(BaseModel):
|
|
93
146
|
|
94
147
|
# Generate example parameters
|
95
148
|
for arg in self.arguments:
|
149
|
+
if properties_injectable.get(arg.name) is not None:
|
150
|
+
continue
|
96
151
|
# Prioritize example, then default, then create a generic example
|
97
152
|
example_value = arg.example or arg.default or f"Your {arg.name} here"
|
98
153
|
markdown += f" <{arg.name}>{example_value}</{arg.name}>\n"
|
@@ -102,6 +157,18 @@ class ToolDefinition(BaseModel):
|
|
102
157
|
|
103
158
|
return markdown
|
104
159
|
|
160
|
+
def get_non_injectable_arguments(self) -> list[ToolArgument]:
|
161
|
+
"""Get arguments that cannot be injected from properties.
|
162
|
+
|
163
|
+
Returns:
|
164
|
+
List of ToolArgument instances that cannot be injected by the agent.
|
165
|
+
"""
|
166
|
+
properties_injectable = self.get_injectable_properties_in_execution()
|
167
|
+
|
168
|
+
return [
|
169
|
+
arg for arg in self.arguments if properties_injectable.get(arg.name) is None
|
170
|
+
]
|
171
|
+
|
105
172
|
|
106
173
|
class Tool(ToolDefinition):
|
107
174
|
"""Extended class for tools with execution capabilities.
|
@@ -144,3 +211,43 @@ class Tool(ToolDefinition):
|
|
144
211
|
A string representing the result of tool execution.
|
145
212
|
"""
|
146
213
|
raise NotImplementedError("This method should be implemented by subclasses.")
|
214
|
+
|
215
|
+
def get_injectable_properties_in_execution(self) -> dict[str, Any]:
|
216
|
+
"""Get injectable properties excluding tool arguments.
|
217
|
+
|
218
|
+
Returns:
|
219
|
+
A dictionary of property names and values, excluding tool arguments and None values.
|
220
|
+
"""
|
221
|
+
# Get argument names from tool definition
|
222
|
+
argument_names = {arg.name for arg in self.arguments}
|
223
|
+
|
224
|
+
# Get properties excluding arguments and filter out None values
|
225
|
+
properties = self.get_properties(exclude=["arguments"])
|
226
|
+
return {
|
227
|
+
name: value
|
228
|
+
for name, value in properties.items()
|
229
|
+
if value is not None and name in argument_names
|
230
|
+
}
|
231
|
+
|
232
|
+
|
233
|
+
if __name__ == "__main__":
|
234
|
+
tool = Tool(name="my_tool", description="A simple tool", arguments=[ToolArgument(name="arg1", arg_type="string")])
|
235
|
+
print(tool.to_markdown())
|
236
|
+
|
237
|
+
class MyTool(Tool):
|
238
|
+
field1: str | None = Field(default=None, description="Field 1 description")
|
239
|
+
|
240
|
+
tool_with_fields = MyTool(
|
241
|
+
name="my_tool1", description="A simple tool", arguments=[ToolArgument(name="field1", arg_type="string")]
|
242
|
+
)
|
243
|
+
print(tool_with_fields.to_markdown())
|
244
|
+
print(tool_with_fields.get_injectable_properties_in_execution())
|
245
|
+
|
246
|
+
tool_with_fields_defined = MyTool(
|
247
|
+
name="my_tool2",
|
248
|
+
description="A simple tool2",
|
249
|
+
field1="field1_value",
|
250
|
+
arguments=[ToolArgument(name="field1", arg_type="string")],
|
251
|
+
)
|
252
|
+
print(tool_with_fields_defined.to_markdown())
|
253
|
+
print(tool_with_fields_defined.get_injectable_properties_in_execution())
|
quantalogic/xml_tool_parser.py
CHANGED
@@ -76,15 +76,17 @@ class ToolParser:
|
|
76
76
|
elements = self.xml_parser.extract_elements(xml_string, preserve_cdata=True)
|
77
77
|
logger.debug(f"Extracted elements from XML: {elements}")
|
78
78
|
|
79
|
+
arguments = self.tool.get_non_injectable_arguments()
|
80
|
+
|
79
81
|
# Check for required arguments
|
80
|
-
for arg in
|
82
|
+
for arg in arguments:
|
81
83
|
if arg.required and arg.name not in elements:
|
82
84
|
error_msg = f"argument {arg.name} not found"
|
83
85
|
logger.error(f"Error extracting XML elements: {error_msg}")
|
84
86
|
raise ValueError(f"Error extracting XML elements: {error_msg}")
|
85
87
|
|
86
88
|
# Create and validate arguments dictionary
|
87
|
-
argument_dict = {arg.name: elements.get(arg.name, "") for arg in
|
89
|
+
argument_dict = {arg.name: elements.get(arg.name, "") for arg in arguments}
|
88
90
|
|
89
91
|
# Validate using Pydantic model
|
90
92
|
validated_args = ToolArguments(arguments=argument_dict)
|