Skip to content

fix: change tool_calls from dict to list - #1435

Open
akihikokuroda wants to merge 9 commits into
generative-computing:mainfrom
akihikokuroda:issue1431
Open

fix: change tool_calls from dict to list#1435
akihikokuroda wants to merge 9 commits into
generative-computing:mainfrom
akihikokuroda:issue1431

Conversation

@akihikokuroda

@akihikokuroda akihikokuroda commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Issue

Fixes #1431

Description

This changed tool_calls from dict to list instead of set because of the follow reason

Aspect Dict Set List
Parallel same-name calls ❌ Collapses ✓ Preserved ✓ Preserved
Execution order ✓ Preserved ❌ Lost ✓ Preserved
Duplicate handling Overwrites silently Deduplicates silently Must validate explicitly
Component prefixing ✓ Works ❌ Order lost ✓ Works
Requires __hash__/__eq__ No Yes No
Equality semantics N/A Unclear N/A
Observability ✓ Simple ❌ Hidden deduplication ✓ Visible duplicates
Implementation effort N/A Medium (add hash/eq) Low (validate helper)

Testing

  • Tests added to the respective file if code was changed
  • New code has 100% coverage if code was added
  • Ensure existing tests and github automation passes (a maintainer will kick off the github automation when the rest of the PR is populated)

Attribution

  • AI coding assistants used

Adding a new component, requirement, sampling strategy, or tool?

If your PR adds or modifies one of the types below, check the matching box. A checklist of type-specific review items will be posted as a comment.

  • Component
  • Requirement
  • Sampling Strategy
  • Tool

NOTE: Please ensure you have an issue that has been acknowledged by a core contributor and routed you to open a pull request against this repository. Otherwise, please open an issue before continuing with this pull request.

@akihikokuroda akihikokuroda changed the title change tool_calls from dict to list fix: change tool_calls from dict to list Jul 23, 2026
@github-actions github-actions Bot added the bug Something isn't working label Jul 23, 2026
@akihikokuroda

Copy link
Copy Markdown
Contributor Author

This has broader changes than expected.

Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
@akihikokuroda
akihikokuroda marked this pull request as ready for review July 29, 2026 14:27
@akihikokuroda
akihikokuroda requested a review from a team as a code owner July 29, 2026 14:27

@jakelorocco jakelorocco left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! Looks mostly good. A few minor things I noticed.

Can you also search through our documentation for these uses? I believe examples like this will need to be modified as well:

response = m.instruct(
    "Use the code interpreter tool to compute 7 factorial.",
    requirements=[uses_tool("python")],
    model_options={ModelOption.TOOLS: [tool]},
    tool_calls=True,
)

# tool_calls=True makes .tool_calls available on the result
code = response.tool_calls["python"].args["code"]
exec_result = response.tool_calls["python"].call_func()
print(exec_result)

Comment thread mellea/backends/utils.py Outdated
Comment on lines +160 to +162
assert isinstance(model_tool_calls, list), (
f"Expected list[ModelToolCall], got {type(model_tool_calls)}"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as prior; must already be a list here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing assertion

Comment thread mellea/backends/litellm.py Outdated
Comment on lines +635 to +641
tool_chunk = extract_model_tool_requests(tools, choice_response)
if tool_chunk is not None:
if mot.tool_calls is None:
mot.tool_calls = {}
# Merge the tool_chunk dict.
for key, val in tool_chunk.items():
mot.tool_calls[key] = val
if not isinstance(tool_chunk, list):
MelleaLogger.get_logger().error(
f"extract_model_tool_requests returned {type(tool_chunk).__name__} "
f"instead of list[ModelToolCall]"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this isn't None, do we need to check that it's a list? Doesn't our type system / the extract_model_tool_requests function ensure it's a list?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is. Removing check.

Comment thread mellea/backends/litellm.py Outdated
Comment on lines +855 to +857
assert isinstance(model_tool_calls, list), (
f"Expected list[ModelToolCall], got {type(model_tool_calls)}"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's possible for this to not be a list here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing assertion

Comment thread mellea/backends/litellm.py Outdated
Comment on lines +810 to +815
@@ -806,38 +811,50 @@ def _extract_model_tool_requests(
self,
tools: dict[str, AbstractMelleaTool],
chat_response: litellm.ModelResponse, # type: ignore
) -> dict[str, ModelToolCall] | None:
model_tool_calls: dict[str, ModelToolCall] = {}
) -> list[ModelToolCall] | None:
model_tool_calls: list[ModelToolCall] = []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we actually using this function anywhere? It looks like this got missed in a refactor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it is not. Removing it.

Comment thread mellea/backends/ollama.py Outdated
Comment on lines +732 to +734
assert isinstance(model_tool_calls, list), (
f"Expected list[ModelToolCall], got {type(model_tool_calls)}"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above. I think this must be a list already.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing check.

Comment thread mellea/backends/openai.py Outdated
Comment on lines +1140 to +1144
if not isinstance(tool_chunk, list):
MelleaLogger.get_logger().error(
f"extract_model_tool_requests returned {type(tool_chunk).__name__} "
f"instead of list[ModelToolCall]"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing check.

Comment thread mellea/backends/tools.py Outdated
Comment on lines +375 to +376
are silently overwritten). For duplicate tool names, use component ID-based prefixing to ensure
unique names across components.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we leave out the component ID based prefixing for now?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed here, I don't think we have component id prefixing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated.

Comment thread mellea/backends/watsonx.py Outdated
Comment on lines +622 to +626
if not isinstance(tool_chunk, list):
MelleaLogger.get_logger().error(
f"extract_model_tool_requests returned {type(tool_chunk).__name__} "
f"instead of list[ModelToolCall]"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing check.

Comment thread mellea/backends/watsonx.py Outdated
Comment on lines +768 to +771
def _extract_model_tool_requests(
self, tools: dict[str, AbstractMelleaTool], chat_response: dict
) -> dict[str, ModelToolCall] | None:
model_tool_calls: dict[str, ModelToolCall] = {}
) -> list[ModelToolCall] | None:
model_tool_calls: list[ModelToolCall] = []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think this function isn't getting used anywhere?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. Removing.

Comment on lines +112 to +114
assert isinstance(model_tool_calls, list), (
f"Expected list[ModelToolCall], got {type(model_tool_calls)}"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as previous comments.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing assertion

@ajbozarth ajbozarth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some feedback from Claude — core dict→list conversion looks correct and complete. Agree with the cleanup items already in the existing review (redundant asserts, unreachable list-guards); not re-posting to avoid duplicate threads. Two test notes to add:

  • No execution-level test for the fix. The added test proves two same-name calls survive extraction, but nothing runs them through _acall_tools to confirm both execute and produce two ToolMessages — the behavior #1431 is about.
  • A round-trip test worth front-loading. Result-turn correlation is #1389's territory and correctly untouched here, but a test now exercising the full tool→model→toolcall→model loop for parallel same-name calls (asserting today's behavior) gives #1389 a regression guard already in place.

@AngeloDanducci AngeloDanducci left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Jake's comments covered everything I found, overall looks good pending the feedback cycle.

Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
@akihikokuroda

Copy link
Copy Markdown
Contributor Author

@ajbozarth a test for "No execution-level test for the fix" is added.

@ajbozarth ajbozarth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review from Claude — thanks for the updates. The cleanup items and the execution-level test are all addressed. One marker fix inline; the fix itself looks good.

from mellea.core.base import ModelOutputThunk, ModelToolCall
from mellea.stdlib.functional import _acall_tools

pytestmark = [pytest.mark.ollama, pytest.mark.e2e]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are integration tests, not e2e/ollama. _acall_tools only touches the backend for formatter.print(), never for inference — they construct ModelToolCalls directly with local Python functions, so no model runs (they pass in ~0.2s with no Ollama server). Per test/README.md, wiring multiple real project components together without external I/O is integration (no backend marker, and not skipped when Ollama is absent) — right now the ollama marker skips this regression guard in every lane without a running Ollama. Suggest pytestmark = [pytest.mark.integration]. The backend fixture (L26) is only used for .formatter.print(), so a real OllamaModelBackend isn't required either.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made changes.

automatic tool execution loop through _acall_tools() correctly processes
multiple same-name tool calls from the list-based tool_calls structure.

Unlike test_parallel_same_name_tool_execution.py which manually calls

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This references test_parallel_same_name_tool_execution.py, which doesn't exist in the tree — dangling reference. Worth removing or pointing at the actual extraction test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made changes, too. Thanks!

Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
@akihikokuroda
akihikokuroda requested a review from ajbozarth July 29, 2026 23:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Duplicate same-name parallel tool calls collapse to a single execution

4 participants