quantalogic 0.2.19__py3-none-any.whl → 0.2.21__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/agent_config.py +7 -4
- quantalogic/coding_agent.py +13 -0
- quantalogic/main.py +9 -6
- quantalogic/server/agent_server.py +1 -1
- 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 +113 -7
- quantalogic/utils/get_coding_environment.py +7 -2
- quantalogic/utils/git_ls.py +27 -6
- quantalogic/xml_tool_parser.py +4 -2
- quantalogic-0.2.21.dist-info/METADATA +517 -0
- {quantalogic-0.2.19.dist-info → quantalogic-0.2.21.dist-info}/RECORD +18 -17
- quantalogic-0.2.19.dist-info/METADATA +0 -992
- {quantalogic-0.2.19.dist-info → quantalogic-0.2.21.dist-info}/LICENSE +0 -0
- {quantalogic-0.2.19.dist-info → quantalogic-0.2.21.dist-info}/WHEEL +0 -0
- {quantalogic-0.2.19.dist-info → quantalogic-0.2.21.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/agent_config.py
CHANGED
@@ -176,7 +176,7 @@ def create_full_agent(
|
|
176
176
|
)
|
177
177
|
|
178
178
|
|
179
|
-
def
|
179
|
+
def create_basic_agent(
|
180
180
|
model_name: str,
|
181
181
|
vision_model_name: str | None = None,
|
182
182
|
no_stream: bool = False,
|
@@ -198,16 +198,19 @@ def create_orchestrator_agent(
|
|
198
198
|
# Rebuild AgentTool to resolve forward references
|
199
199
|
AgentTool.model_rebuild()
|
200
200
|
|
201
|
-
coding_agent_instance = create_coding_agent(model_name)
|
202
201
|
|
203
202
|
tools = [
|
204
203
|
TaskCompleteTool(),
|
205
204
|
ListDirectoryTool(),
|
206
205
|
ReadFileBlockTool(),
|
207
|
-
RipgrepTool(),
|
208
206
|
SearchDefinitionNames(),
|
207
|
+
ReadFileTool(),
|
208
|
+
ReplaceInFileTool(),
|
209
|
+
WriteFileTool(),
|
210
|
+
EditWholeContentTool(),
|
211
|
+
ReplaceInFileTool(),
|
212
|
+
ExecuteBashCommandTool(),
|
209
213
|
LLMTool(model_name=model_name, on_token=console_print_token if not no_stream else None),
|
210
|
-
AgentTool(agent=coding_agent_instance, agent_role="software expert", name="coder_agent_tool"),
|
211
214
|
]
|
212
215
|
|
213
216
|
if vision_model_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/main.py
CHANGED
@@ -31,7 +31,7 @@ from quantalogic.agent_config import ( # noqa: E402
|
|
31
31
|
create_coding_agent,
|
32
32
|
create_full_agent,
|
33
33
|
create_interpreter_agent,
|
34
|
-
|
34
|
+
create_basic_agent,
|
35
35
|
)
|
36
36
|
from quantalogic.interactive_text_editor import get_multiline_input # noqa: E402
|
37
37
|
from quantalogic.search_agent import create_search_agent # noqa: E402
|
@@ -52,7 +52,7 @@ def create_agent_for_mode(mode: str, model_name: str, vision_model_name: str | N
|
|
52
52
|
if mode == "code-basic":
|
53
53
|
return create_coding_agent(model_name, vision_model_name, basic=True, no_stream=no_stream, compact_every_n_iteration=compact_every_n_iteration, max_tokens_working_memory=max_tokens_working_memory)
|
54
54
|
elif mode == "basic":
|
55
|
-
return
|
55
|
+
return create_basic_agent(model_name, vision_model_name, no_stream=no_stream, compact_every_n_iteration=compact_every_n_iteration, max_tokens_working_memory=max_tokens_working_memory)
|
56
56
|
elif mode == "full":
|
57
57
|
return create_full_agent(model_name, vision_model_name, no_stream=no_stream, compact_every_n_iteration=compact_every_n_iteration, max_tokens_working_memory=max_tokens_working_memory)
|
58
58
|
elif mode == "interpreter":
|
@@ -159,7 +159,8 @@ def display_welcome_message(
|
|
159
159
|
vision_model_name: str | None,
|
160
160
|
max_iterations: int = 50,
|
161
161
|
compact_every_n_iteration: int | None = None,
|
162
|
-
max_tokens_working_memory: int | None = None
|
162
|
+
max_tokens_working_memory: int | None = None,
|
163
|
+
mode: str = "basic"
|
163
164
|
) -> None:
|
164
165
|
"""Display the welcome message and instructions."""
|
165
166
|
version = get_version()
|
@@ -174,6 +175,7 @@ def display_welcome_message(
|
|
174
175
|
"\n"
|
175
176
|
f"- Model: {model_name}\n"
|
176
177
|
f"- Vision Model: {vision_model_name}\n"
|
178
|
+
f"- Mode: {mode}\n"
|
177
179
|
f"- Max Iterations: {max_iterations}\n"
|
178
180
|
f"- Memory Compact Frequency: {compact_every_n_iteration or 'Default (Max Iterations)'}\n"
|
179
181
|
f"- Max Working Memory Tokens: {max_tokens_working_memory or 'Default'}\n\n"
|
@@ -208,7 +210,7 @@ def display_welcome_message(
|
|
208
210
|
help="Set logging level (info/debug/warning).",
|
209
211
|
)
|
210
212
|
@click.option("--verbose", is_flag=True, help="Enable verbose output.")
|
211
|
-
@click.option("--mode", type=click.Choice(AGENT_MODES), default="
|
213
|
+
@click.option("--mode", type=click.Choice(AGENT_MODES), default="basic", help="Agent mode (code/search/full).")
|
212
214
|
@click.option(
|
213
215
|
"--vision-model-name",
|
214
216
|
default=None,
|
@@ -266,7 +268,7 @@ def cli(
|
|
266
268
|
help='Specify the model to use (litellm format, e.g. "openrouter/deepseek/deepseek-chat").',
|
267
269
|
)
|
268
270
|
@click.option("--verbose", is_flag=True, help="Enable verbose output.")
|
269
|
-
@click.option("--mode", type=click.Choice(AGENT_MODES), default="
|
271
|
+
@click.option("--mode", type=click.Choice(AGENT_MODES), default="basic", help="Agent mode (code/search/full).")
|
270
272
|
@click.option(
|
271
273
|
"--log",
|
272
274
|
type=click.Choice(["info", "debug", "warning"]),
|
@@ -333,7 +335,8 @@ def task(
|
|
333
335
|
vision_model_name,
|
334
336
|
max_iterations=max_iterations,
|
335
337
|
compact_every_n_iteration=compact_every_n_iteration,
|
336
|
-
max_tokens_working_memory=max_tokens_working_memory
|
338
|
+
max_tokens_working_memory=max_tokens_working_memory,
|
339
|
+
mode=mode
|
337
340
|
)
|
338
341
|
check_new_version()
|
339
342
|
logger.debug("Waiting for user input...")
|
@@ -28,7 +28,7 @@ from quantalogic.agent_config import (
|
|
28
28
|
MODEL_NAME,
|
29
29
|
create_agent,
|
30
30
|
create_coding_agent, # noqa: F401
|
31
|
-
|
31
|
+
create_basic_agent, # noqa: F401
|
32
32
|
)
|
33
33
|
from quantalogic.console_print_events import console_print_events
|
34
34
|
|
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
@@ -27,9 +27,10 @@ class ToolArgument(BaseModel):
|
|
27
27
|
)
|
28
28
|
description: str | None = Field(default=None, description="A brief description of the argument.")
|
29
29
|
required: bool = Field(default=False, description="Indicates if the argument is required.")
|
30
|
-
default: str | None = Field(
|
30
|
+
default: str | None = Field(
|
31
|
+
default=None, description="The default value for the argument. This parameter is required."
|
32
|
+
)
|
31
33
|
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
34
|
|
34
35
|
|
35
36
|
class ToolDefinition(BaseModel):
|
@@ -42,12 +43,49 @@ class ToolDefinition(BaseModel):
|
|
42
43
|
need_validation: Flag to indicate if tool requires validation.
|
43
44
|
"""
|
44
45
|
|
45
|
-
model_config = ConfigDict(extra="
|
46
|
+
model_config = ConfigDict(extra="allow", validate_assignment=True)
|
46
47
|
|
47
48
|
name: str = Field(..., description="The unique name of the tool.")
|
48
49
|
description: str = Field(..., description="A brief description of what the tool does.")
|
49
50
|
arguments: list[ToolArgument] = Field(default_factory=list, description="A list of arguments the tool accepts.")
|
50
|
-
need_validation: bool = Field(
|
51
|
+
need_validation: bool = Field(
|
52
|
+
default=False,
|
53
|
+
description="When True, requires user confirmation before execution. Useful for tools that perform potentially destructive operations.",
|
54
|
+
)
|
55
|
+
need_variables: bool = Field(
|
56
|
+
default=False,
|
57
|
+
description="When True, provides access to the agent's variable store. Required for tools that need to interpolate variables (e.g., Jinja templates).",
|
58
|
+
)
|
59
|
+
need_caller_context_memory: bool = Field(
|
60
|
+
default=False,
|
61
|
+
description="When True, provides access to the agent's conversation history. Useful for tools that need context from previous interactions.",
|
62
|
+
)
|
63
|
+
|
64
|
+
def get_properties(self, exclude: list[str] | None = None) -> dict[str, Any]:
|
65
|
+
"""Return a dictionary of all non-None properties, excluding Tool class fields and specified fields.
|
66
|
+
|
67
|
+
Args:
|
68
|
+
exclude: Optional list of field names to exclude from the result
|
69
|
+
|
70
|
+
Returns:
|
71
|
+
Dictionary of property names and values, excluding Tool class fields and specified fields.
|
72
|
+
"""
|
73
|
+
exclude = exclude or []
|
74
|
+
tool_fields = {
|
75
|
+
"name",
|
76
|
+
"description",
|
77
|
+
"arguments",
|
78
|
+
"need_validation",
|
79
|
+
"need_variables",
|
80
|
+
"need_caller_context_memory",
|
81
|
+
}
|
82
|
+
properties = {}
|
83
|
+
|
84
|
+
for name, value in self.__dict__.items():
|
85
|
+
if name not in tool_fields and name not in exclude and value is not None and not name.startswith("_"):
|
86
|
+
properties[name] = value
|
87
|
+
|
88
|
+
return properties
|
51
89
|
|
52
90
|
def to_json(self) -> str:
|
53
91
|
"""Convert the tool to a JSON string representation.
|
@@ -67,10 +105,21 @@ class ToolDefinition(BaseModel):
|
|
67
105
|
markdown = f"`{self.name}`:\n"
|
68
106
|
markdown += f"- **Description**: {self.description}\n\n"
|
69
107
|
|
108
|
+
properties_injectable = self.get_injectable_properties_in_execution()
|
109
|
+
|
70
110
|
# Parameters section
|
71
111
|
if self.arguments:
|
72
112
|
markdown += "- **Parameters**:\n"
|
113
|
+
parameters = ""
|
73
114
|
for arg in self.arguments:
|
115
|
+
# Skip if parameter name matches an object property with non-None value
|
116
|
+
# This enables automatic property injection during execution:
|
117
|
+
# When an object has a property matching an argument name,
|
118
|
+
# the agent will inject the property value at runtime,
|
119
|
+
# reducing manual input and improving flexibility
|
120
|
+
if properties_injectable.get(arg.name) is not None:
|
121
|
+
continue
|
122
|
+
|
74
123
|
required_status = "required" if arg.required else "optional"
|
75
124
|
# Prioritize example, then default, then create a generic description
|
76
125
|
value_info = ""
|
@@ -79,12 +128,15 @@ class ToolDefinition(BaseModel):
|
|
79
128
|
elif arg.default is not None:
|
80
129
|
value_info = f" (default: `{arg.default}`)"
|
81
130
|
|
82
|
-
|
131
|
+
parameters += (
|
83
132
|
f" - `{arg.name}`: "
|
84
133
|
f"({required_status}{value_info})\n"
|
85
|
-
f" {arg.description or '
|
134
|
+
f" {arg.description or ''}\n"
|
86
135
|
)
|
87
|
-
|
136
|
+
if len(parameters) > 0:
|
137
|
+
markdown += parameters + "\n\n"
|
138
|
+
else:
|
139
|
+
markdown += "None\n\n"
|
88
140
|
|
89
141
|
# Usage section with XML-style example
|
90
142
|
markdown += "**Usage**:\n"
|
@@ -93,6 +145,8 @@ class ToolDefinition(BaseModel):
|
|
93
145
|
|
94
146
|
# Generate example parameters
|
95
147
|
for arg in self.arguments:
|
148
|
+
if properties_injectable.get(arg.name) is not None:
|
149
|
+
continue
|
96
150
|
# Prioritize example, then default, then create a generic example
|
97
151
|
example_value = arg.example or arg.default or f"Your {arg.name} here"
|
98
152
|
markdown += f" <{arg.name}>{example_value}</{arg.name}>\n"
|
@@ -102,6 +156,18 @@ class ToolDefinition(BaseModel):
|
|
102
156
|
|
103
157
|
return markdown
|
104
158
|
|
159
|
+
def get_non_injectable_arguments(self) -> list[ToolArgument]:
|
160
|
+
"""Get arguments that cannot be injected from properties.
|
161
|
+
|
162
|
+
Returns:
|
163
|
+
List of ToolArgument instances that cannot be injected by the agent.
|
164
|
+
"""
|
165
|
+
properties_injectable = self.get_injectable_properties_in_execution()
|
166
|
+
|
167
|
+
return [
|
168
|
+
arg for arg in self.arguments if properties_injectable.get(arg.name) is None
|
169
|
+
]
|
170
|
+
|
105
171
|
|
106
172
|
class Tool(ToolDefinition):
|
107
173
|
"""Extended class for tools with execution capabilities.
|
@@ -144,3 +210,43 @@ class Tool(ToolDefinition):
|
|
144
210
|
A string representing the result of tool execution.
|
145
211
|
"""
|
146
212
|
raise NotImplementedError("This method should be implemented by subclasses.")
|
213
|
+
|
214
|
+
def get_injectable_properties_in_execution(self) -> dict[str, Any]:
|
215
|
+
"""Get injectable properties excluding tool arguments.
|
216
|
+
|
217
|
+
Returns:
|
218
|
+
A dictionary of property names and values, excluding tool arguments and None values.
|
219
|
+
"""
|
220
|
+
# Get argument names from tool definition
|
221
|
+
argument_names = {arg.name for arg in self.arguments}
|
222
|
+
|
223
|
+
# Get properties excluding arguments and filter out None values
|
224
|
+
properties = self.get_properties(exclude=["arguments"])
|
225
|
+
return {
|
226
|
+
name: value
|
227
|
+
for name, value in properties.items()
|
228
|
+
if value is not None and name in argument_names
|
229
|
+
}
|
230
|
+
|
231
|
+
|
232
|
+
if __name__ == "__main__":
|
233
|
+
tool = Tool(name="my_tool", description="A simple tool", arguments=[ToolArgument(name="arg1", arg_type="string")])
|
234
|
+
print(tool.to_markdown())
|
235
|
+
|
236
|
+
class MyTool(Tool):
|
237
|
+
field1: str | None = Field(default=None, description="Field 1 description")
|
238
|
+
|
239
|
+
tool_with_fields = MyTool(
|
240
|
+
name="my_tool1", description="A simple tool", arguments=[ToolArgument(name="field1", arg_type="string")]
|
241
|
+
)
|
242
|
+
print(tool_with_fields.to_markdown())
|
243
|
+
print(tool_with_fields.get_injectable_properties_in_execution())
|
244
|
+
|
245
|
+
tool_with_fields_defined = MyTool(
|
246
|
+
name="my_tool2",
|
247
|
+
description="A simple tool2",
|
248
|
+
field1="field1_value",
|
249
|
+
arguments=[ToolArgument(name="field1", arg_type="string")],
|
250
|
+
)
|
251
|
+
print(tool_with_fields_defined.to_markdown())
|
252
|
+
print(tool_with_fields_defined.get_injectable_properties_in_execution())
|
@@ -1,15 +1,20 @@
|
|
1
1
|
import os
|
2
2
|
|
3
|
+
from loguru import logger
|
4
|
+
|
3
5
|
from quantalogic.utils.get_environment import get_environment
|
4
6
|
from quantalogic.utils.git_ls import git_ls
|
5
7
|
|
6
8
|
|
7
9
|
def get_coding_environment() -> str:
|
8
10
|
"""Retrieve coding environment details."""
|
9
|
-
|
11
|
+
logger.debug("Retrieving coding environment details.")
|
12
|
+
result = (
|
10
13
|
f"{get_environment()}"
|
11
14
|
"\n\n"
|
12
15
|
"<codebase_first_level>\n"
|
13
|
-
f"{git_ls(directory_path=os.getcwd())}"
|
16
|
+
f"{git_ls(directory_path=os.getcwd(), recursive=False, max_depth=1)}"
|
14
17
|
"\n</codebase_first_level>\n"
|
15
18
|
)
|
19
|
+
logger.debug(f"Coding environment details:\n{result}")
|
20
|
+
return result
|