Skip to content

feat: add prefix to component tool names - #1432

Open
akihikokuroda wants to merge 17 commits into
generative-computing:mainfrom
akihikokuroda:issue95-1
Open

feat: add prefix to component tool names#1432
akihikokuroda wants to merge 17 commits into
generative-computing:mainfrom
akihikokuroda:issue95-1

Conversation

@akihikokuroda

@akihikokuroda akihikokuroda commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Issue

Fixes #95

Description

  1. Auto-prefixing of component tools (mellea/backends/tools.py):

    • Component tools are automatically prefixed with component_{COMPONENT_ID}__ to prevent naming collisions
    • COMPONENT_ID is derived from Python object identity: hex(id(component))[-8:]
    • This provides multi-turn stability: the same component object produces the same ID across calls
  2. JSON schema updates (mellea/backends/tools.py):

    • Modified convert_tools_to_json() to ensure function names in JSON schemas match the prefixed keys
    • The model now sees and requests the prefixed tool names
  3. TemplateRepresentation enhancements (mellea/core/base.py):

    • Added tool_name_mapping field to track original → prefixed name mappings
    • Populated during tool extraction, defaults to None
  4. Test coverage:

    • Updated existing tests to validate prefixed tool names
    • Verified that duplicate tool names across components are preserved with prefixes

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.

@github-actions github-actions Bot added the enhancement New feature or request label Jul 23, 2026
@akihikokuroda
akihikokuroda marked this pull request as ready for review July 23, 2026 15:01
@akihikokuroda
akihikokuroda requested a review from a team as a code owner July 23, 2026 15:01
@jakelorocco

Copy link
Copy Markdown
Contributor

@akihikokuroda, we should create a full proposal for this. I don't know if we want to prefix component tools with an index. I think we need to ensure that performance isn't impacted. We also need to make sure that we can clearly link tools to any given component, which might require more testing / planning.

@ajbozarth

Copy link
Copy Markdown
Contributor

Small note on just an initial pass, I think this will have conflicts will #1430 adding tool_call_id

@akihikokuroda

Copy link
Copy Markdown
Contributor Author

@jakelorocco Here is a proposal.

Proposed Solution: Object ID-Based Prefixing

Design

Use Python object identity as the stable component identifier:

component_id = hex(id(component))[-8:]  # e.g., "a1b2c3d4"
prefixed_name = f"component_{component_id}.{original_tool_name}"

Properties

Property Value
Uniqueness Guaranteed per object instance (Python guarantees)
Stability Stable for object lifetime (same object = same ID across turns)
Determinism Deterministic within a single generation
Serialization Non-serializable (acceptable; tools don't cross process boundaries)
Name length ~16 chars (component_a1b2c3d4.search) vs 8 chars for index

Example: Multi-Turn Scenario with ID-Based Prefixing

Turn 1: User provides [SearchAgent@0x7f123abc, RetrievalAgent@0x7f456def, FilterAgent@0x7f789ghi]

  IDs (last 8 hex chars):
    - SearchAgent: a1b2c3d4
    - RetrievalAgent: e5f6g7h8
    - FilterAgent: i9j0k1l2

  LLM sees tools:
    - component_a1b2c3d4.search
    - component_e5f6g7h8.retrieve
    - component_i9j0k1l2.filter

Turn 2: User reorders context [FilterAgent@0x7f789ghi, SearchAgent@0x7f123abc, RetrievalAgent@0x7f456def]

  Same objects, same IDs:
    - FilterAgent: i9j0k1l2 (unchanged)
    - SearchAgent: a1b2c3d4 (unchanged)
    - RetrievalAgent: e5f6g7h8 (unchanged)

  LLM sees tools:
    - component_i9j0k1l2.filter   ← SAME NAME 
    - component_a1b2c3d4.search   ← SAME NAME 
    - component_e5f6g7h8.retrieve ← SAME NAME 

STABLE: Tool names unchanged regardless of component order!

Advantages Over Index-Based

1. Multi-Turn Stability

  • Tool names remain constant across turns if components are reused
  • LLM can reference same tool by same name in follow-up interactions
  • No confusion from component reordering

2. Prevents Parallel Tool Collisions

3. Component Context Independence

  • Same component reused in different contexts gets same tool names
  • Tool identity follows component object, not context position
  • Enables composition scenarios where agents are mixed/reordered

4. Better Observability

  • Tool prefix is stable and traceable
  • Can correlate with component metadata (stored on TemplateRepresentation)
  • Trace spans show consistent component identity

5. Integrates with PR #1430 (Tool Tracing)

  • tool_call_id (provider ID) + component ID prefix = full observability chain

Performance Impact

Analysis

Operation Complexity Cost
Generate component ID O(1) hex(id(component))[-8:] ≈ 1µs
Prefix tool name O(1) String concatenation ≈ 0.1µs per tool
JSON schema update O(1) Name field update in dict copy
Tool lookup in dict O(1) Hash lookup unchanged

Example with 10 tools:

  • Index-based: ~1µs per tool = ~10µs total
  • ID-based: ~1.1µs per tool = ~11µs total
  • Difference: <1µs negligible

Token Cost

Name length comparison:

  • Index-based: component0.search (16 chars)
  • ID-based: component_a1b2c3d4.search (26 chars)
  • Difference: ~10 chars per tool

Example with 10 tools in JSON schema:

  • Index-based: ~160 chars
  • ID-based: ~260 chars
  • Token cost: ~0.05% increase (negligible for typical prompts)

Conclusion: Performance impact is negligible. No performance concerns.

  • Can link tool execution to component type and provider trace

About concern:

We also need to make sure that we can clearly link tools to any given component, which might require more testing / planning

Better Solution: Metadata on TemplateRepresentation

Instead of reverse mapping, store component metadata on TemplateRepresentation:

@dataclass                                                                                                                                                                                                
class TemplateRepresentation:                                                                                                                                                                             
    obj: Any                                                                                                                                                                                              
    args: dict[...]                                                                                                                                                                                       
    tools: dict[str, AbstractMelleaTool] | None = None                                                                                                                                                    
    tool_name_mapping: dict[str, str] | None = None                                                                                                                                                       
                                                                                                                                                                                                          
    # NEW: Component identity metadata                                                                                                                                                                    
    component_id: str | None = None  # e.g., "a1b2c3d4"                                                                                                                                                   
    component_type: str | None = None  # e.g., "SearchAgent"                                                                                                                                              
    component_description: str | None = None  # Optional                                                                                                                                                  

Why This is Better

Aspect Reverse Mapping Metadata on TR Winner
Storage Dict management Field on dataclass Metadata
Lifecycle Complex (when create/clear?) Simple (lives with TR) Metadata
Synchronization Must stay in sync with tools Atomic with tools Metadata
Serialization Can't serialize Can serialize Metadata
Observability Must access object Already available Metadata
API coupling Exposes component objects No coupling Metadata
Performance O(1) lookup (rarely used) O(1) access Neutral

Usage Example

# At tool registration time:                                                                                                                                                                              
if isinstance(action, Component):                                                                                                                                                                         
    tr = action.format_for_llm()                                                                                                                                                                          
    if isinstance(tr, TemplateRepresentation):                                                                                                                                                            
        tr.component_id = hex(id(action))[-8:]                                                                                                                                                            
        tr.component_type = type(action).__name__                                                                                                                                                         
        # tool_name_mapping already set                                                                                                                                                                   
        # All metadata in one place ✅                                                                                                                                                                    
                                                                                                                                                                                                          
# At trace time:                                                                                                                                                                                          
span.set_attribute("mellea.component.id", tr.component_id)                                                                                                                                                
span.set_attribute("mellea.component.type", tr.component_type)                                                                                                                                            
# Metadata available without reverse mapping ✅                                                                                                                                                           
                                                                                                                                                                                                          
# In error messages:                                                                                                                                                                                      
error = f"Tool {tool_name} failed (component: {tr.component_type})"                                                                                                                                       
# Clear attribution without reverse mapping ✅                                                                                                                                                            
                                                                                                                                                                                                          
# For debugging:                                                                                                                                                                                          
# User reads error message, sees component type, can correlate                                                                                                                                            
# No need to reverse-map tool name back to component ✅                                                                                                                                                   

@jakelorocco

Copy link
Copy Markdown
Contributor

@akihikokuroda, I think this is still a good idea but we probably need some evaluations to indicate that the model can properly track the difference between components. Especially since we don't print component ids when outputting them currently.

@akihikokuroda

Copy link
Copy Markdown
Contributor Author

@jakelorocco I added examples. docs/examples/components/pattern2_context_and_tools.py shows the component tools renaming, executing and telemetry output.

@planetf1 planetf1 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.

Thanks for tackling the tool-name-collision problem (#95) — the underlying idea (auto-prefix component tools so they don't clobber each other) is sound. A few things worth resolving before this merges, most importantly a correctness bug that would break real OpenAI/Azure usage but is currently invisible to CI because the only exercising examples are Ollama-gated.

Two non-code notes:

  • The PR description says tools are prefixed with component{N}. (sequential index, e.g. component0.search), but the shipped code uses component_{hex(id(action))[-8:]}. (object-identity hash). Worth updating the description so reviewers/future readers aren't misled.
  • Small thing, but since this will likely be squash-merged and the PR title becomes the commit message: "prfefix" → "prefix" in the title itself would be good to fix before merging.

Comment thread mellea/backends/tools.py Outdated
Comment thread mellea/backends/tools.py
Comment thread mellea/backends/tools.py Outdated
Comment thread mellea/telemetry/metrics.py Outdated
Comment thread docs/examples/components/README.md Outdated
Comment thread docs/examples/components/README.md Outdated
from mellea.core.base import AbstractMelleaTool
from mellea.formatters import TemplateFormatter
from mellea.stdlib.context import ChatContext
from mellea.stdlib.functional import _call_tools

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 imports the private mellea.stdlib.functional._call_tools directly, and it's used nowhere else outside functional.py itself (only transform() calls it internally, e.g. functional.py:469). Docs/examples are what people copy from, so this is teaching users to reach into an implementation detail. If the goal is just "call the LLM and run any tools it invokes," this example can go through m.transform(...) / act(..., tool_calls=True), which already does this internally. If there's a genuine need to trigger tool execution manually outside of transform/act, that's a gap worth closing with a public helper rather than exporting from examples via a private import.

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.

It is not true that the act() is calling _call_tools.

Function File Line Calls _call_tools? Executes Tools?
session.act() session.py 482 ❌ No ❌ No
mfuncs.act() functional.py 89 ❌ No ❌ No
aact() functional.py 576 ❌ No ❌ No
backend.generate_from_context() backends/*.py - ❌ No ❌ No
transform() functional.py 426 ✅ Yes (line 469) ✅ Yes
query() functional.py - ✅ Yes ✅ Yes
React framework frameworks/react.py 123 ✅ Yes ✅ Yes

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.

The two private example files have now gone, but the README still retains their sections and run commands. It also recommends calling _call_tools() directly, including in the general guidance, even though call_tools() is public and the two remaining examples use it. Please remove the stale private-example sections and replace the remaining _call_tools() recommendations with call_tools().

from mellea.core.base import AbstractMelleaTool
from mellea.formatters import TemplateFormatter
from mellea.stdlib.context import ChatContext
from mellea.stdlib.functional import _call_tools

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 private-import concern as in duplicate_tool_names.py:33 — see that comment.

Comment thread mellea/telemetry/metrics.py Outdated
@jakelorocco

Copy link
Copy Markdown
Contributor

@jakelorocco I added examples. docs/examples/components/pattern2_context_and_tools.py shows the component tools renaming, executing and telemetry output.

It's more an issue of us ensuring this approach works long term and is resilient. If we go this route, we need data / benchmarks to prove that this approach works.

We also have no way to correlate the components that are being output as messages with the prefixed tools still.

@akihikokuroda

Copy link
Copy Markdown
Contributor Author

@jakelorocco This should be a bug fix. Can we leave this bug as is?

It's more an issue of us ensuring this approach works long term and is resilient. If we go this route, we need data / benchmarks to prove that this approach works.

I don't have idea what data / benchmarks needs. Please advise.

The component doesn't have name. How is it identified?

We also have no way to correlate the components that are being output as messages with the prefixed tools still.

I wonder what this correlation is used for?

Thanks!

@akihikokuroda akihikokuroda changed the title feat: add prfefix to component tool names feat: add prefix to component tool names Jul 27, 2026
@jakelorocco

Copy link
Copy Markdown
Contributor

@jakelorocco This should be a bug fix. Can we leave this bug as is?

It's more an issue of us ensuring this approach works long term and is resilient. If we go this route, we need data / benchmarks to prove that this approach works.

I don't have idea what data / benchmarks needs. Please advise.

The component doesn't have name. How is it identified?

We also have no way to correlate the components that are being output as messages with the prefixed tools still.

I wonder what this correlation is used for?

Thanks!

I think these things should be addressed before merging. I think potential issues related to the prefixes are large enough that we ought to ensure the approach works across a wide range of situations before committing to it.

Tools that come from components usually relate to that component. So the fact that we don't have a way to match a tool prefix to its serialized component message seems problematic.

The fact that we don't know if this holds up for more complicated examples where many components are in scope / being added. For instance, if we ask a model to transpose all charts, is it smart enough to make that same tool call multiple times with the different prefixes. Do we need to add language to a system prompt to explain how to use the component-prefixed tools?

I'm asking these questions because this likely isn't the only way to implement a feature like this. For example, instead, we could force tool calls to require an object identifier as a parameter instead of appending multiple of the "same" tools. An approach like that might actually work better by potentially reducing the context required to encode all the tools.

@ajbozarth

Copy link
Copy Markdown
Contributor

I'll hold off further PR review until we address the design questions Jake raised, feel free to loop me in if you have a call to discuss things

@akihikokuroda
akihikokuroda marked this pull request as draft July 27, 2026 18:26
@akihikokuroda

Copy link
Copy Markdown
Contributor Author

Prototyped the "we could force tool calls to require an object identifier as a parameter"

It hits an issue and here is the analysis.

The issue is that only ONE tool is in the tools dict (['query']), so the LLM doesn't know there are two different "query" tools from different components. It just sees one tool and calls it.

This reveals a fundamental problem with the parameter approach when tools have duplicate names:

  • ✅ Works well when you have one component per tool name
  • ❌ Breaks down when multiple components have the same tool name because only the last one survives in the dict

With multiple components having the same tool name, we need a different strategy:

  1. Either keep them both in the dict using a different key structure (like prefixing)
  2. Or provide a way for the LLM to differentiate between them (maybe through better prompting or tool descriptions)

The parameter approach I implemented assumes one-to-one tool names. To truly support multiple components with identical tool names, we'd need either:

  • The prefix approach (component_xxx__query, component_yyy__query)
  • Or a list-based tools structure instead of a dict

Here is the list-based implementation analysis:

Tool Structure: Where It's Defined and Used

Current Structure

1. Definition

File: mellea/core/base.py:1710

class TemplateRepresentation:
    tools: dict[str, AbstractMelleaTool] | None = None

Type: dict[str, AbstractMelleaTool]

  • Keys: Tool name (string)
  • Values: AbstractMelleaTool instance
  • Problem: Dict only allows ONE tool per name

2. Tool Extraction

File: mellea/backends/tools.py:367

def add_tools_from_context_actions(
    tools_dict: dict[str, AbstractMelleaTool],
    ctx_actions: list[Component | CBlock | ModelOutputThunk] | None,
):
    for action in ctx_actions:
        tr = action.format_for_llm()
        for tool_name, func in tr.tools.items():
            wrapped_tool = wrap_tool_with_component_id(func, component_id)
            tools_dict[tool_name] = wrapped_tool  # ← OVERWRITES duplicates!

3. Tool Usage

File: mellea/backends/tools.py:430

def convert_tools_to_json(tools: dict[str, AbstractMelleaTool]) -> list[dict]:
    return [t.as_json_tool for t in tools.values()]

Converts dict values to JSON array for LLM.

4. Backend Tool Passing

Example: mellea/backends/ollama.py:496

chat_response = self._async_client.chat(
    model=self._model_id,
    messages=conversation,
    tools=[t.as_json_tool for t in tools.values()],  # ← Dict values to list
    ...
)

The Problem with Dict Structure

When multiple components have the same tool name:

  1. First component's tool: tools_dict["query"] = tool_1
  2. Second component's tool: tools_dict["query"] = tool_2OVERWRITES tool_1
  3. Result: Only tool_2 exists in the dict

Solutions

Option A: Keep Dict, Add Prefixes (Current PR #1432 approach)

tools_dict["component_abc123__query"] = tool_1
tools_dict["component_def456__query"] = tool_2

Pros: Works, but tool names become verbose
Cons: Breaks with specific prompts (MObject issue)

Option B: Change to List-Based Structure

@dataclass
class TemplateRepresentation:
    tools: list[tuple[str, AbstractMelleaTool]] | None = None

Pros: Preserves all tools, natural names + parameters work
Cons: Breaks all existing code using tools dict

Option C: Dict with Composite Keys

tools_dict[(component_id, "query")] = tool

Pros: No overwrites, clean structure
Cons: Changes API, breaks backwards compat

Option D: Dict of Lists

tools_dict["query"] = [tool_1, tool_2, ...]

Pros: Simple, preserves all tools
Cons: Requires changes to tool extraction and usage

Files That Need Changes

If changing to list-based (Option B):

  1. mellea/core/base.py - TemplateRepresentation.tools type
  2. mellea/backends/tools.py - add_tools_from_context_actions()
  3. mellea/backends/tools.py - convert_tools_to_json()
  4. All backends (ollama, openai, litellm, etc.)
  5. Any code iterating over tools dict

Current Parameter Approach Impact:

  • ✅ Works with single tool per name
  • ❌ Fails with multiple components having same tool name (dict collision)
  • ✅ component_id parameter IS added correctly
  • ✅ Works for MObject (single component)

Recommended Fix

To make parameter approach work with multiple tools:
Change tools structure from dict to list to preserve all tools, then use component_id parameters to route correctly.

@jakelorocco

Copy link
Copy Markdown
Contributor

Makes sense; I was just trying to describe that there are multiple ways to do something like this. And this is a big enough change that we should test out multiple options and ensure the chosen approach works in many different scenarios.

We don't quite have a process for that right now; but moving this to a discussion might help. We can also probably figure out what performance metrics we are looking for with a change like this.

@akihikokuroda

Copy link
Copy Markdown
Contributor Author

Discussion (#1455) is created.

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>
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 August 27, 2026 19:37
@akihikokuroda

Copy link
Copy Markdown
Contributor Author

Mark this as ready for review. discussions are in #1455

Comment thread mellea/telemetry/metrics.py

@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 (posted by Alex). Core mechanism looks correct, comments inline. One item that can't be line-anchored: the PR description still describes a tool_name_mapping field and component0.search naming, neither of which exists in the code (the fields added are component_id/component_type/component_description, and the prefix is component_{hexid}__name). Please update the description to match.

Comment thread mellea/backends/tools.py Outdated
Comment thread mellea/backends/tools.py
Comment thread mellea/backends/tools.py
Comment thread mellea/core/base.py Outdated
Comment thread mellea/telemetry/metrics.py Outdated
Comment thread docs/examples/components/duplicate_tool_names.py Outdated
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>

@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 (posted by Alex). 1, 2, 4 look resolved — thanks. Three still open, inline.

Comment thread mellea/backends/tools.py
Comment thread mellea/backends/tools.py Outdated
Comment thread docs/examples/components/duplicate_tool_names.py Outdated
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>

@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.

This LGTM with one docstring nit. I'll leave @planetf1 and @jakelorocco to re-review for final merge since they previously pushed back on the original implementation.

Comment thread mellea/telemetry/metrics_plugins.py Outdated
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
@planetf1

planetf1 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

The PR description still needs updating before approval. The current implementation uses component_{id}__{name}, not component0.search; it has no tool_name_mapping field; and the tests cover the ID-based double-underscore form. Please rewrite the summary to match the code.

Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

allow tools to be renamed

4 participants