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 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, # New parameter
87
- max_tokens_working_memory: int | None = None, # New parameter to set max working memory tokens
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=VariableMemory(),
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
@@ -176,7 +176,7 @@ def create_full_agent(
176
176
  )
177
177
 
178
178
 
179
- def create_orchestrator_agent(
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:
@@ -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
- create_orchestrator_agent,
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 create_orchestrator_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)
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="code", help="Agent mode (code/search/full).")
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="code", help="Agent mode (code/search/full).")
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
- create_orchestrator_agent, # noqa: F401
31
+ create_basic_agent, # noqa: F401
32
32
  )
33
33
  from quantalogic.console_print_events import console_print_events
34
34
 
@@ -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())
@@ -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
- return self._format_results(results, directory_path, language_name, file_pattern, output_format)
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\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\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(default=None, description="The default value for the argument. This parameter is required.")
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="forbid", validate_assignment=True)
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(default=False, description="Indicates if the tool needs validation.")
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
- markdown += (
131
+ parameters += (
83
132
  f" - `{arg.name}`: "
84
133
  f"({required_status}{value_info})\n"
85
- f" {arg.description or 'No description provided.'}\n"
134
+ f" {arg.description or ''}\n"
86
135
  )
87
- markdown += "\n"
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
- return (
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