Repository: dynamiq-ai/dynamiq
Pull request #223: fix: default function calling schema
Author: login: maksymbuleshnyi
Created: 2025-04-01T10:30:47Z | Merged: 2025-04-03T17:35:05Z


<!-- ELLIPSIS_HIDDEN -->


> [!IMPORTANT]
> Enhances JSON parsing error handling and updates function calling schema in ReAct agent to support tools with and without input parameters.
> 
>   - **Error Handling**:
>     - Adds error handling for JSON parsing in `_run_agent()` in `react.py` to raise `ActionParsingException` if `action_input` or `llm_generated_output` cannot be parsed.
>   - **Function Calling Schema**:
>     - Updates `generate_function_calling_schemas()` in `react.py` to handle tools with and without input parameters, creating different schemas based on the presence of input parameters.
>   - **Misc**:
>     - Modifies `execute()` in `base.py` to set `stop` to `None` if not provided.
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=dynamiq-ai%2Fdynamiq&utm_source=github&utm_medium=referral)<sup> for 94b0914ba3c11ac35576c37a350a53575d89675c. It will automatically update as commits are pushed.</sup>


<!-- ELLIPSIS_HIDDEN -->


Diff:
diff --git a/dynamiq/nodes/agents/react.py b/dynamiq/nodes/agents/react.py
index 2b04a5e4d..7b624a317 100644
--- a/dynamiq/nodes/agents/react.py
+++ b/dynamiq/nodes/agents/react.py
@@ -145,12 +145,18 @@
 
 
 REACT_BLOCK_INSTRUCTIONS_FUNCTION_CALLING = """
-You have to call appropriate functions.
+You need to use the right functions based on what the user asks.
 
-Function descriptions:
-plan_next_action - function that should be called to use tools [{tools_name}]].
-provide_final_answer - function that should be called when answer on initial request can be provided.
-Call this function if initial user input does not have any actionable request.
+Use the function `provide_final_answer` when you can give a clear answer to the user's first question,
+ and no extra steps, tools, or work are needed.
+Call this function if the user's input is simple and doesn’t require additional help or tools.
+
+If the user's request requires the use of specific tools, such as [{tools_name}],
+ you must first call the appropriate function to invoke those tools.
+Only after utilizing the necessary tools and gathering the required information should
+ you call `provide_final_answer` to deliver the final response.
+
+Make sure to check each request carefully to see if you can answer it right away or if you need to use tools to help.
 """  # noqa: E501
 
 
@@ -548,6 +554,14 @@ def _run_agent(
                         thought = llm_generated_output_json["thought"]
                         action_input = llm_generated_output_json["action_input"]
 
+                        if isinstance(action_input, str):
+                            try:
+                                action_input = json.loads(action_input)
+                            except json.JSONDecodeError as e:
+                                raise ActionParsingException(
+                                    f"Error parsing action_input string. {e}", recoverable=True
+                                )
+
                         self.log_reasoning(thought, action, action_input, loop_num)
 
                         if self.streaming.enabled and self.streaming.mode == StreamingMode.ALL:
@@ -566,7 +580,10 @@ def _run_agent(
 
                         llm_generated_output = llm_result.output["content"]
                         self.tracing_intermediate(loop_num, self._prompt.messages, llm_generated_output)
-                        llm_generated_output_json = json.loads(llm_generated_output)
+                        try:
+                            llm_generated_output_json = json.loads(llm_generated_output)
+                        except json.JSONDecodeError as e:
+                            raise ActionParsingException(f"Error parsing action. {e}", recoverable=True)
 
                         thought = llm_generated_output_json["thought"]
                         action = llm_generated_output_json["action"]
@@ -585,7 +602,11 @@ def _run_agent(
                                 )
                             return action_input
 
-                        action_input = json.loads(action_input)
+                        try:
+                            action_input = json.loads(action_input)
+                        except json.JSONDecodeError as e:
+                            raise ActionParsingException(f"Error parsing action_input string. {e}", recoverable=True)
+
                         self.log_reasoning(thought, action, action_input, loop_num)
 
                         if self.streaming.enabled and self.streaming.mode == StreamingMode.ALL:
@@ -810,37 +831,67 @@ def generate_function_calling_schemas(self):
         self.format_schema.append(final_answer_function_schema)
         for tool in self.tools:
             properties = {}
-            for name, field in tool.input_schema.model_fields.items():
-                self.generate_property_schema(properties, name, field)
-
-            schema = {
-                "type": "function",
-                "function": {
-                    "name": self.sanitize_tool_name(tool.name),
-                    "description": tool.description[:1024],
-                    "parameters": {
-                        "type": "object",
-                        "properties": {
-                            "thought": {
-                                "type": "string",
-                                "description": "Your reasoning about why you can answer original question.",
+
+            input_params = tool.input_schema.model_fields.items()
+            if list(input_params):
+                for name, field in tool.input_schema.model_fields.items():
+                    self.generate_property_schema(properties, name, field)
+
+                schema = {
+                    "type": "function",
+                    "function": {
+                        "name": self.sanitize_tool_name(tool.name),
+                        "description": tool.description[:1024],
+                        "parameters": {
+                            "type": "object",
+                            "properties": {
+                                "thought": {
+                                    "type": "string",
+                                    "description": "Your reasoning about using this tool.",
+                                },
+                                "action_input": {
+                                    "type": "object",
+                                    "description": "Input for the selected tool",
+                                    "properties": properties,
+                                    "required": list(properties.keys()),
+                                    "additionalProperties": False,
+                                },
                             },
-                            "action_input": {
-                                "type": "object",
-                                "description": "Input for chosen action.",
-                                "properties": properties,
-                                "required": list(properties.keys()),
-                                "additionalProperties": False,
+                            "additionalProperties": False,
+                            "required": ["thought", "action_input"],
+                        },
+                        "strict": True,
+                    },
+                }
+
+                self.format_schema.append(schema)
+
+            else:
+                schema = {
+                    "type": "function",
+                    "function": {
+                        "name": self.sanitize_tool_name(tool.name),
+                        "description": tool.description[:1024],
+                        "parameters": {
+                            "type": "object",
+                            "properties": {
+                                "thought": {
+                                    "type": "string",
+                                    "description": "Your reasoning about using this tool.",
+                                },
+                                "action_input": {
+                                    "type": "string",
+                                    "description": "Input for the selected tool in JSON string format.",
+                                },
                             },
+                            "additionalProperties": False,
+                            "required": ["thought", "action_input"],
                         },
-                        "additionalProperties": False,
-                        "required": ["thought", "action_input"],
+                        "strict": True,
                     },
-                    "strict": True,
-                },
-            }
+                }
 
-            self.format_schema.append(schema)
+                self.format_schema.append(schema)
 
     def _init_prompt_blocks(self):
         """Initialize the prompt blocks required for the ReAct strategy."""
diff --git a/dynamiq/nodes/llms/base.py b/dynamiq/nodes/llms/base.py
index 95adb8613..c595afb01 100644
--- a/dynamiq/nodes/llms/base.py
+++ b/dynamiq/nodes/llms/base.py
@@ -330,7 +330,7 @@ def execute(
             "max_tokens": self.max_tokens,
             "tools": tools,
             "tool_choice": self.tool_choice,
-            "stop": self.stop,
+            "stop": self.stop if self.stop else None,
             "top_p": self.top_p,
             "seed": self.seed,
             "presence_penalty": self.presence_penalty,


Reviews:
user: login: ellipsis-dev[bot]
body: :x: Changes requested. Reviewed everything up to 09f4dc3735a572c07d5411f5b05df8905263e1b5 in <ellipsistime>2 minutes and 18 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `141` lines of code in `1` files
 - Skipped `0` files when reviewing.
 - Skipped posting `6` drafted comments based on config settings.


<details>
<summary>1. <code>dynamiq/nodes/agents/react.py:552</code></summary>

- **Draft comment:** 
Good defensive coding: wrapping the json.loads on action_input in a try/except is a safe improvement. Consider catching json.JSONDecodeError specifically to avoid catching unrelated exceptions.
- **Reason this comment was not posted:** 
Confidence changes required: `50%` <= threshold `50%`
None

</details>

<details>
<summary>2. <code>dynamiq/nodes/agents/react.py:576</code></summary>

- **Draft comment:** 
Adding a try/except for json.loads of llm_generated_output is a good improvement; again, consider catching json.JSONDecodeError to be more specific.
- **Reason this comment was not posted:** 
Confidence changes required: `50%` <= threshold `50%`
None

</details>

<details>
<summary>3. <code>dynamiq/nodes/agents/react.py:147</code></summary>

- **Draft comment:** 
Good update to the instructions text; the new wording is clearer. Ensure its style remains consistent with other similar instruction blocks.
- **Reason this comment was not posted:** 
Confidence changes required: `0%` <= threshold `50%`
None

</details>

<details>
<summary>4. <code>dynamiq/nodes/agents/react.py:575</code></summary>

- **Draft comment:** 
Similarly, in the try block for parsing 'llm_generated_output', consider catching json.JSONDecodeError specifically to avoid masking unrelated exceptions.
- **Reason this comment was not posted:** 
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 30% vs. threshold = 50%
The suggestion is reasonable from a code quality perspective - catching specific exceptions is generally better practice than catching generic Exception. However, the current code handles the error appropriately by wrapping it in an ActionParsingException with a recoverable flag. The change would be a minor improvement but not critical. The code works correctly either way.
The current code might accidentally catch and mask other types of exceptions that should be handled differently. The suggestion could prevent masking of unrelated errors.
While catching specific exceptions is better practice, in this case the error handling is appropriate - any parsing error is wrapped in ActionParsingException and marked as recoverable. The code has a clear intent to handle any parsing failure the same way.
While the suggestion follows good practices, the current error handling is appropriate for its purpose. The comment suggests a minor improvement that isn't strictly necessary.

</details>

<details>
<summary>5. <code>dynamiq/nodes/agents/react.py:822</code></summary>

- **Draft comment:** 
In generate_function_calling_schemas, the check 'if input_params:' relies on dict_items which is empty if no fields exist, but consider converting it to a list (e.g. if list(input_params): ...) for clarity and avoid iterating over model_fields twice. Also verify that using a string for 'action_input' in the empty case is the intended behavior.
- **Reason this comment was not posted:** 
Marked as duplicate.

</details>

<details>
<summary>6. <code>dynamiq/nodes/agents/react.py:226</code></summary>

- **Draft comment:** 
Typo in the 'description' field of final_answer_function_schema: the sentence "Function should be called when if you can answer the initial request or if there is not request at all." is a bit off. Consider rephrasing it to something like "Function should be called when you can answer the initial request, or if there is no request at all."
- **Reason this comment was not posted:** 
Comment was not on a location in the diff, so it can't be submitted as a review comment.

</details>


Workflow ID: <workflowid>`wflow_RG8JHCTI7LTc5dwp`</workflowid>

</details>


----
**Want Ellipsis to fix these issues?** Tag `@ellipsis-dev` in a comment. You can customize Ellipsis with :+1: / :-1: [feedback](https://docs.ellipsis.dev/review), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T10:33:10Z
user: login: olbychos
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T12:06:04Z
user: login: ellipsis-dev[bot]
body: :+1: Looks good to me! Incremental review on d0e1e25af45f3fa08efe07646394c557a214d58c in <ellipsistime>1 minute and 38 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `229` lines of code in `8` files
 - Skipped `0` files when reviewing.
 - Skipped posting `12` drafted comments based on config settings.


<details>
<summary>1. <code>dynamiq/nodes/utils/utils.py:22</code></summary>

- **Draft comment:** 
Renaming the field from 'schema' to 'json_schema' and using the alias 'schema' is consistent with similar changes across the codebase. Consider updating the class docstrings to reflect this change for clarity.
- **Reason this comment was not posted:** 
Comment was not on a location in the diff, so it can't be submitted as a review comment.

</details>

<details>
<summary>2. <code>dynamiq/connections/connections.py:1042</code></summary>

- **Draft comment:** 
Renaming the field from 'schema' to 'snowflake_schema' and using alias='schema' clarifies its purpose and avoids naming conflicts. Ensure all references are updated accordingly.
- **Reason this comment was not posted:** 
Comment looked like it was already resolved.

</details>

<details>
<summary>3. <code>dynamiq/nodes/agents/base.py:95</code></summary>

- **Draft comment:** 
Updating 'model_observation' to 'agent_model_observation' with alias='model_observation' maintains external API consistency while improving internal naming clarity.
- **Reason this comment was not posted:** 
Comment did not seem useful. Confidence is useful = `0%` <= threshold `50%`
This comment is purely informative, explaining the reason behind a change without suggesting any action or asking for confirmation. It doesn't align with the rules for useful comments, as it doesn't provide a suggestion, ask for a test, or confirm an intentional change.

</details>

<details>
<summary>4. <code>dynamiq/nodes/agents/react.py:425</code></summary>

- **Draft comment:** 
Using model_dump(by_alias=True) ensures that field aliases (e.g., for agent_model_observation) are included in the output, which is crucial for the function calling schema.
- **Reason this comment was not posted:** 
Comment did not seem useful. Confidence is useful = `0%` <= threshold `50%`
This comment is purely informative, explaining the use of `model_dump(by_alias=True)`. It doesn't suggest any changes or ask for confirmation on the intention. It seems to violate the rule against purely informative comments.

</details>

<details>
<summary>5. <code>dynamiq/nodes/tools/e2b_sandbox.py:93</code></summary>

- **Draft comment:** 
Using 'bytes | io.BytesIO' within isinstance may not work as expected. Consider using a tuple: isinstance(file, (bytes, io.BytesIO)).
- **Reason this comment was not posted:** 
Comment was not on a location in the diff, so it can't be submitted as a review comment.

</details>

<details>
<summary>6. <code>dynamiq/nodes/utils/utils.py:22</code></summary>

- **Draft comment:** 
Renaming the field from 'schema' to 'json_schema' (with alias 'schema') in utility nodes aligns with the updated function calling schema convention, ensuring consistency across the codebase.
- **Reason this comment was not posted:** 
Comment did not seem useful. Confidence is useful = `0%` <= threshold `50%`
This comment is purely informative, explaining the reason behind a change without suggesting any action or asking for confirmation. It doesn't align with the rules for useful comments.

</details>

<details>
<summary>7. <code>dynamiq/nodes/agents/base.py:24</code></summary>

- **Draft comment:** 
Consider updating the phrase in AGENT_PROMPT_TEMPLATE (line 24) from "You are AI powered assistant." to "You are an AI-powered assistant." for improved grammar and style.
- **Reason this comment was not posted:** 
Comment was not on a location in the diff, so it can't be submitted as a review comment.

</details>

<details>
<summary>8. <code>dynamiq/nodes/agents/base.py:659</code></summary>

- **Draft comment:** 
In the 'tool_description' property (line 659), the string uses a closing tag formatted as `<\{tool.name}_description>`. Consider changing it to a more conventional closing tag format such as `</{tool.name}_description>` for clarity and consistency.
- **Reason this comment was not posted:** 
Comment was not on a location in the diff, so it can't be submitted as a review comment.

</details>

<details>
<summary>9. <code>dynamiq/nodes/agents/react.py:230</code></summary>

- **Draft comment:** 
The description in final_answer_function_schema currently reads: "Function should be called when if you can answer the initial request or if there is not request at all." Consider rephrasing to something like: "Function should be called when you can answer the initial request or if there is no request at all." This will improve clarity.
- **Reason this comment was not posted:** 
Comment was not on a location in the diff, so it can't be submitted as a review comment.

</details>

<details>
<summary>10. <code>dynamiq/nodes/tools/exa_search.py:74</code></summary>

- **Draft comment:** 
The description for 'use_autoprompt' concatenates two strings without a space, resulting in '...converted to a Exa query.Enabled by default...'. Also, consider changing 'a Exa query' to 'an Exa query' for grammatical correctness.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>11. <code>dynamiq/nodes/tools/exa_search.py:80</code></summary>

- **Draft comment:** 
The description for 'query_type' is split into multiple concatenated strings which results in missing spaces between sentences (e.g. '...auto'.Neural uses an embeddings-based model...). Please add appropriate spacing between the concatenated parts.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>

<details>
<summary>12. <code>dynamiq/nodes/tools/exa_search.py:87</code></summary>

- **Draft comment:** 
In the 'category' field description, the concatenated strings result in missing spaces between sentences (e.g. '...to focus on.Options are company...'). Please insert a space between the sentences to improve readability.
- **Reason this comment was not posted:** 
Comment was on unchanged code.

</details>


Workflow ID: <workflowid>`wflow_WyiLzXymXZpAGJbI`</workflowid>

</details>


----
You can customize Ellipsis with :+1: / :-1: [feedback](https://docs.ellipsis.dev/review), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T10:44:14Z
user: login: olbychos
body: Does this fix include solutions for image input?
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T12:19:26Z
user: login: ellipsis-dev[bot]
body: :+1: Looks good to me! Incremental review on ea139128c98855d7b3139d836445c523604b598c in <ellipsistime>1 minute and 16 seconds</ellipsistime>

<details>
<summary>More details</summary>

 - Looked at `13` lines of code in `1` files
 - Skipped `0` files when reviewing.
 - Skipped posting `2` drafted comments based on config settings.


<details>
<summary>1. <code>dynamiq/nodes/llms/base.py:330</code></summary>

- **Draft comment:** 
Ensure that using 'if self.stop else None' doesn't unintentionally discard a valid empty list if that's needed.
- **Reason this comment was not posted:** 
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 10% vs. threshold = 50%
The change seems intentional - it converts any falsy value (including empty lists) to None. However, looking at the type hints, self.stop is explicitly typed as list[str] | None, suggesting that None is a valid value and empty lists are not meant to be passed through. The change aligns with the type hints. The comment seems to be speculating about a edge case without strong evidence that empty lists are valid or needed.
I could be wrong about the semantics of the stop parameter - maybe empty lists have special meaning in the LLM API that's different from None.
The type hints are explicit that None is valid, and there's no evidence in the code that empty lists have special meaning. The change appears to be enforcing the type contract.
Delete the comment. The change appears intentional and aligns with type hints. There's no strong evidence that empty lists need special handling.

</details>

<details>
<summary>2. <code>dynamiq/nodes/llms/base.py:332</code></summary>

- **Draft comment:** 
Consider using the more concise 'self.stop or None' expression instead of 'self.stop if self.stop else None'. Also, ensure that converting an empty list to None is intentional, as an empty list might sometimes be a valid input.
- **Reason this comment was not posted:** 
Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 20% vs. threshold = 50%
The comment has two parts: 1) A style suggestion to use 'or' instead of 'if/else' 2) A concern about empty lists. The style suggestion is valid - both expressions are equivalent and 'or' is more idiomatic Python. However, the concern about empty lists is not relevant since self.stop is explicitly typed as list[str] | None, meaning empty lists are not a valid state.
The comment raises a valid point about code style, but the concern about empty lists shows a misunderstanding of the type system. Also, is this style change important enough to warrant a comment?
While the style suggestion is technically correct, it's a minor optimization that doesn't significantly impact code quality or readability. Both forms are perfectly readable and maintainable.
The comment should be deleted. While technically correct about the style suggestion, it's too minor to warrant a comment, and the concern about empty lists is incorrect given the type definition.

</details>


Workflow ID: <workflowid>`wflow_TLjUUkWFoLzFYMuV`</workflowid>

</details>


----
You can customize Ellipsis with :+1: / :-1: [feedback](https://docs.ellipsis.dev/review), review rules, user-specific overrides, `quiet` mode, and [more](https://docs.ellipsis.dev/config).

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T16:46:26Z
user: login: olbychos
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T11:46:28Z

Review comments:
diff_hunk: @@ -810,37 +822,67 @@ def generate_function_calling_schemas(self):
         self.format_schema.append(final_answer_function_schema)
         for tool in self.tools:
             properties = {}
-            for name, field in tool.input_schema.model_fields.items():
-                self.generate_property_schema(properties, name, field)
-
-            schema = {
-                "type": "function",
-                "function": {
-                    "name": self.sanitize_tool_name(tool.name),
-                    "description": tool.description[:1024],
-                    "parameters": {
-                        "type": "object",
-                        "properties": {
-                            "thought": {
-                                "type": "string",
-                                "description": "Your reasoning about why you can answer original question.",
+
+            input_params = tool.input_schema.model_fields.items()
+            if input_params:
path: dynamiq/nodes/agents/react.py
user: login: ellipsis-dev[bot]
body: The condition `if input_params:` will always evaluate to True since `dict_items` is truthy even for empty dictionaries. Use `if list(input_params):` or check the length explicitly to correctly handle cases with no input schema fields.
```suggestion
            if list(input_params):
```
created_at: 2025-04-01T10:33:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -548,6 +549,14 @@
                         thought = llm_generated_output_json["thought"]
                         action_input = llm_generated_output_json["action_input"]
 
+                        if isinstance(action_input, str):
+                            try:
+                                action_input = json.loads(action_input)
+                            except Exception as e:
path: dynamiq/nodes/agents/react.py
user: login: ellipsis-dev[bot]
body: When parsing a string `action_input`, consider catching `json.JSONDecodeError` specifically instead of a generic `Exception` for more targeted error handling.
```suggestion
                            except json.JSONDecodeError as e:
```
created_at: 2025-04-01T10:33:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -147,10 +147,11 @@
 REACT_BLOCK_INSTRUCTIONS_FUNCTION_CALLING = """
 You have to call appropriate functions.
 
-Function descriptions:
-plan_next_action - function that should be called to use tools [{tools_name}]].
-provide_final_answer - function that should be called when answer on initial request can be provided.
-Call this function if initial user input does not have any actionable request.
+Use the function provide_final_answer when a conclusive response to the user's
path: dynamiq/nodes/agents/react.py
user: login: olbychos
body: Let's clarify the wording a bit here: 
```
REACT_BLOCK_INSTRUCTIONS_FUNCTION_CALLING = """
You need to use the right functions based on what the user asks.

Use the function `provide_final_answer` when you can give a clear answer to the user's first question, and no extra steps, tools, or work are needed. Call this function if the user's input is simple and doesn’t require additional help or tools.

If the user's request requires the use of specific tools, such as [{tools_name}], you must first call the appropriate function to invoke those tools. Only after utilizing the necessary tools and gathering the required information should you call `provide_final_answer` to deliver the final response.

Make sure to check each request carefully to see if you can answer it right away or if you need to use tools to help.
"""
```
created_at: 2025-04-01T12:06:00Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-01T10:35:28Z
body: <!-- Pytest Coverage Comment: coverage -->
<a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/README.md"><img alt="Coverage" src="https://img.shields.io/badge/Coverage-68%25-yellow.svg" /></a><details><summary>Coverage Report • </summary><table><tr><th>File</th><th>Stmts</th><th>Miss</th><th>Cover</th><th>Missing</th></tr><tbody><tr><td colspan="5"><b>dynamiq/nodes/agents</b></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py">react.py</a></td><td>301</td><td>246</td><td>18%</td><td><a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L278">278</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L295">295</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L305-L308">305&ndash;308</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L310-L313">310&ndash;313</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L315">315</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L319-L320">319&ndash;320</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L324-L327">324&ndash;327</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L329-L336">329&ndash;336</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L338">338</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L343">343</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L345-L349">345&ndash;349</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L366">366</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L368">368</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L372-L374">372&ndash;374</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L378">378</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L384-L385">384&ndash;385</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L387">387</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L391-L392">391&ndash;392</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L397-L401">397&ndash;401</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L403-L404">403&ndash;404</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L406-L408">406&ndash;408</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L420">420</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L423">423</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L448-L449">448&ndash;449</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L451">451</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L459-L460">459&ndash;460</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L462">462</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L464-L467">464&ndash;467</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L469-L471">469&ndash;471</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L478-L480">478&ndash;480</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L485">485</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L487-L489">487&ndash;489</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L491">491</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L493-L498">493&ndash;498</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L506">506</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L508-L509">508&ndash;509</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L511-L512">511&ndash;512</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L521-L523">521&ndash;523</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L525-L527">525&ndash;527</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L531-L532">531&ndash;532</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L536">536</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L538">538</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L540-L545">540&ndash;545</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L552">552</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L554-L555">554&ndash;555</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L557-L561">557&ndash;561</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L565">565</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L567-L568">567&ndash;568</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L577-L579">577&ndash;579</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L581-L586">581&ndash;586</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L588-L590">588&ndash;590</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L592-L596">592&ndash;596</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L603">603</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L605-L608">605&ndash;608</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L610">610</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L612-L613">612&ndash;613</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L622-L624">622&ndash;624</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L626-L627">626&ndash;627</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L629-L634">629&ndash;634</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L641">641</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L643-L644">643&ndash;644</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L646-L647">646&ndash;647</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L656">656</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L658-L659">658&ndash;659</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L661-L663">661&ndash;663</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L665-L666">665&ndash;666</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L668-L670">668&ndash;670</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L679">679</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L687">687</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L689-L690">689&ndash;690</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L693">693</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L699">699</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L701-L702">701&ndash;702</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L707">707</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L709-L711">709&ndash;711</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L718">718</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L724-L727">724&ndash;727</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L729">729</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L733-L734">733&ndash;734</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L738-L744">738&ndash;744</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L746">746</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L748-L749">748&ndash;749</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L751-L752">751&ndash;752</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L755">755</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L757">757</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L784">784</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L797-L798">797&ndash;798</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L800">800</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L803-L804">803&ndash;804</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L806-L807">806&ndash;807</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L809">809</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L811-L813">811&ndash;813</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L815-L816">815&ndash;816</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L818-L819">818&ndash;819</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L822-L823">822&ndash;823</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L825-L827">825&ndash;827</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L831-L833">831&ndash;833</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L835-L838">835&ndash;838</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L840">840</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L867">867</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L870">870</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L894">894</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L898">898</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L900">900</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L906-L911">906&ndash;911</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L913-L915">913&ndash;915</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L917-L918">917&ndash;918</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/agents/react.py#L922">922</a></td></tr><tr><td colspan="5"><b>dynamiq/nodes/llms</b></td></tr><tr><td>&nbsp; &nbsp;<a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/llms/base.py">base.py</a></td><td>130</td><td>15</td><td>88%</td><td><a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/llms/base.py#L14">14</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/llms/base.py#L47">47</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/llms/base.py#L53">53</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/llms/base.py#L199-L204">199&ndash;204</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/llms/base.py#L262-L264">262&ndash;264</a>, <a href="https://github.com/dynamiq-ai/dynamiq/blob/94b0914ba3c11ac35576c37a350a53575d89675c/dynamiq/nodes/llms/base.py#L266-L268">266&ndash;268</a></td></tr><tr><td><b>TOTAL</b></td><td><b>12811</b></td><td><b>4098</b></td><td><b>68%</b></td><td>&nbsp;</td></tr></tbody></table></details>

| Tests | Skipped | Failures | Errors | Time |
| ----- | ------- | -------- | -------- | ------------------ |
| 465 | 0 :zzz: | 0 :x: | 0 :fire: | 43.112s :stopwatch: |

author_association: NONE
user: login: maksymbuleshnyi
created_at: 2025-04-02T21:29:20Z
body: > Does this fix include solutions for image input?

Yes
author_association: CONTRIBUTOR