"""Regex pattern-based search for tool discovery."""
import re
import logging
from typing import TYPE_CHECKING
from ..config import config
if TYPE_CHECKING:
from ..catalog import ToolCatalog, ToolDefinition
logger = logging.getLogger(__name__)
class RegexSearchError(Exception):
"""Error during regex search."""
def __init__(self, message: str, error_code: str):
super().__init__(message)
self.error_code = error_code
class RegexSearch:
"""Regex pattern-based search for tools.
Matches patterns against tool names and descriptions using
Python's re.search() with configurable flags.
Compatible with Anthropic's tool_search_tool_regex_20251119.
"""
def __init__(self, catalog: "ToolCatalog"):
"""Initialize regex search engine.
Args:
catalog: The tool catalog to search
"""
self.catalog = catalog
def search(
self,
pattern: str,
top_k: int | None = None,
case_insensitive: bool = False,
) -> list["ToolDefinition"]:
"""Search for tools matching the regex pattern.
Args:
pattern: Python regex pattern (re.search() syntax)
top_k: Maximum number of results (defaults to config)
case_insensitive: Whether to use case-insensitive matching
Returns:
List of matching tools
Raises:
RegexSearchError: If pattern is invalid or too long
"""
if top_k is None:
top_k = config.MAX_SEARCH_RESULTS
# Validate pattern length
if len(pattern) > config.REGEX_PATTERN_MAX_LENGTH:
raise RegexSearchError(
f"Pattern exceeds {config.REGEX_PATTERN_MAX_LENGTH} character limit",
error_code="pattern_too_long"
)
# Compile pattern
try:
flags = re.IGNORECASE if case_insensitive else 0
compiled = re.compile(pattern, flags)
except re.error as e:
raise RegexSearchError(
f"Invalid regex pattern: {e}",
error_code="invalid_pattern"
)
# Search through tools
matches: list["ToolDefinition"] = []
for tool in self.catalog.list_tools():
# Search in name and description
if compiled.search(tool.name) or compiled.search(tool.description):
matches.append(tool)
if len(matches) >= top_k:
break
logger.debug(f"Regex search '{pattern}' found {len(matches)} matches")
return matches
def search_names(
self,
pattern: str,
top_k: int | None = None,
case_insensitive: bool = False,
) -> list[str]:
"""Search and return only tool names.
Args:
pattern: Python regex pattern
top_k: Maximum number of results
case_insensitive: Whether to use case-insensitive matching
Returns:
List of matching tool names
"""
tools = self.search(pattern, top_k, case_insensitive)
return [tool.name for tool in tools]
@staticmethod
def validate_pattern(pattern: str) -> tuple[bool, str | None]:
"""Validate a regex pattern without searching.
Args:
pattern: The pattern to validate
Returns:
Tuple of (is_valid, error_message)
"""
if len(pattern) > config.REGEX_PATTERN_MAX_LENGTH:
return False, f"Pattern exceeds {config.REGEX_PATTERN_MAX_LENGTH} character limit"
try:
re.compile(pattern)
return True, None
except re.error as e:
return False, f"Invalid regex: {e}"