Tool Bridge¶
Overview¶
agents/tool_bridge.py connects the LLM agent to codemol's tool system. It handles two tasks:
- Schema generation — convert tool modules into OpenAI function-calling schemas
- 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:
Parsing¶
The LLM sends:
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():
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 Å"