Back to all posts
AI & ML

Building an AI SOC Analyst Agent Without Frameworks

Part 2: Tool Registries and the Dispatch Pattern

View Code on GitHub

Series: Building an AI SOC Analyst Agent Without Frameworks · Part 2 of 6

  1. 1. Part 1: What AI Agents Actually Are
  2. 2. Part 2: Tool Registries and the Dispatch Pattern
  3. 3. Part 3: The Tools: Giving the Agent Eyes and Memory
  4. 4. Part 4: Prompt Engineering for AI Agents Is Different
  5. 5. Part 5: Bulletproofing the Infrastructure (Config & Logging)
  6. 6. Part 6: Testing AI Agents Without Hitting APIs

Once the agent could request tools, I needed a way to execute them.

The model returns the name of the function it wants to call, along with its arguments. My application then has to map that function name to the correct Python implementation.

At first, the obvious solution seemed to be a series of if/elif statements:

def execute_tool(tool_name, arguments):
    if tool_name == "check_ip_reputation":
        return check_ip_reputation(**arguments)
    elif tool_name == "query_logs":
        return query_logs(**arguments)
    else:
        raise ValueError(f"Unknown tool: {tool_name}")

This works when you have two or three tools. It becomes increasingly difficult to maintain as the agent grows.

Every new tool requires modifying the dispatcher, violating the Open/Closed Principle: software should be open for extension but closed for modification.

The dispatcher also becomes tightly coupled to every tool implementation. As more capabilities are added, the file grows into a long chain of conditional statements that is harder to read, test, and maintain.

Instead, I adopted the Tool Registry pattern.

The idea is simple: maintain a dictionary that maps tool names to their corresponding Python functions.

TOOL_REGISTRY = {
    "check_ip_reputation": check_ip_reputation,
    "query_logs": query_logs,
}

Executing a tool then becomes generic:

def execute_tool(tool_name, arguments):
	func = TOOL_REGISTRY.get(tool_name)
	if not func:
		raise ValueError(f"Unknown tool: {tool_name}")
	
	return func(**arguments) # this unpacks the arguments dict directly into the function.

Notice that the dispatcher never needs to know how each tool works. It only performs one responsibility: routing a tool name to the appropriate function.

Adding a new capability now requires only two steps:

  1. Implement the new tool.
  2. Register it in TOOL_REGISTRY.

The dispatcher itself never changes.

This separation of responsibilities also made the project structure much cleaner:

Each module has a single responsibility, making the system easier to understand and extend.

Although this is a relatively small design decision, it reflects an important principle when building AI agents: the agent’s capabilities will continue to grow. A dispatch mechanism that works for three tools often becomes a maintenance burden when the agent has twenty.

The Tool Registry pattern allows the architecture to scale without continually rewriting the execution logic. It’s a simple pattern, but one that keeps the codebase modular, extensible, and easier to maintain as new tools are added.