Skip to content

Tool Bridge

Overview

agents/tool_bridge.py connects the LLM agent to codemol's tool system. It handles two tasks:

  1. Schema generation — convert tool modules into OpenAI function-calling schemas
  2. Execution — parse LLM tool calls and dispatch them

Schema Generation

build_tool_schemas() creates one function schema per tool group:

def build_tool_schemas(tools_dict, tool_groups=None) -> list[dict]:
    """Build OpenAI-compatible function schemas from tool registry."""

Schema Format

Each tool group becomes a single function with an input parameter:

{
    "type": "function",
    "function": {
        "name": "measurements",
        "description": "Tool group: measurements.\nAvailable sub-tools: angle, contacts, dihedral, distance, ...",
        "parameters": {
            "type": "object",
            "properties": {
                "input": {
                    "type": "string",
                    "description": "Sub-tool name followed by arguments.\nExamples:\n  distance A/45/CA B/120/CA\n  angle A/45/CA A/45/C A/45/N"
                }
            },
            "required": ["input"]
        }
    }
}

Why one function per group?

Grouping tools reduces the schema size and makes it easier for the LLM to navigate — instead of 200+ individual functions, it sees ~27 groups with sub-tool documentation.

Tool Group Filtering

When an agent has tool_groups set, only matching groups are included in the schema:

# Trajectory agent only sees:
schemas = build_tool_schemas(tools, tool_groups=["trajectory", "analysis", "representations"])

Tool Execution

execute_tool_call() parses the LLM's function call and dispatches it:

def execute_tool_call(tools_dict, cmd, function_name, arguments, scope=None) -> str:

Parsing

The LLM sends:

{"name": "measurements", "arguments": {"input": "distance A/45/CA B/120/CA"}}

The bridge parses this with shlex.split:

parts = shlex.split(arguments["input"])
# ["distance", "A/45/CA", "B/120/CA"]
sub_tool = parts[0]   # "distance"
args = parts[1:]       # ["A/45/CA", "B/120/CA"]

Dispatch

Then calls the standard dispatch():

result = dispatch(tools_dict, function_name, sub_tool, args, cmd, scope=scope)

Signal Filtering

Some tool results are UI-only signals (e.g., __PICK_DISTANCE__ for interactive picking). The bridge filters these out since the agent can't interact with UI:

if result and result.startswith("__PICK_"):
    return "This tool requires interactive picking (not available in agent mode)"

End-to-End Flow

sequenceDiagram
    participant LLM
    participant Bridge as tool_bridge.py
    participant Dispatch as tool_loader.dispatch
    participant Tool as Tool Module
    participant Engine

    LLM->>Bridge: call("measurements", {"input": "distance A/45/CA B/120/CA"})
    Bridge->>Bridge: shlex.split → ["distance", "A/45/CA", "B/120/CA"]
    Bridge->>Dispatch: dispatch(tools, "measurements", "distance", [...], cmd, scope)
    Dispatch->>Dispatch: fit_args, resolve aliases, scope
    Dispatch->>Tool: run(cmd, "A/45/CA", "B/120/CA")
    Tool->>Engine: cmd.distance(...)
    Tool-->>Dispatch: "Distance: 3.45 Å"
    Dispatch-->>Bridge: "Distance: 3.45 Å"
    Bridge-->>LLM: "Distance: 3.45 Å"