mcp_ / utils /tool_manager.py
eienmojiki's picture
x
44e7605
# utils/tool_manager.py
import inspect
import re
# A dictionary to store registered tools
# Key: Internal function name (str)
# Value: Dictionary { 'name': display_name (str), 'func': tool_function (callable), 'ui_builder': ui_builder_function (callable) }
_tool_registry = {}
def tool(name: str, control_components):
"""
Decorator to register a tool function and its UI builder.
Args:
name (str): The display name of the tool in the UI dropdown.
control_components (callable): A function that builds the Gradio UI
components (input, output, button) for this tool.
This function should return a tuple of
(ui_group, input_components, output_components, button_component).
The ui_group should be a gr.Group or similar container
that holds all the tool's UI and whose visibility can be toggled.
"""
def decorator(func):
# Store the function and its metadata in the registry
if func.__name__ in _tool_registry:
print(f"Warning: Tool '{func.__name__}' is already registered. Overwriting.")
_tool_registry[func.__name__] = {
"name": name,
"func": func,
"ui_builder": control_components
}
print(f"Registered tool: {name} (Internal name: {func.__name__})")
# Return the original function so it can be called
return func
return decorator
def get_tool_registry():
"""Returns the dictionary of registered tools."""
return _tool_registry
def format_docstring_as_markdown(docstring: str | None) -> str:
"""
Converts a standard Python function docstring into a Markdown-formatted string.
Handles:
- Formatting the first sentence/paragraph as a heading (##).
- Bolding standard sections like "Args:", "Returns:", "Raises:".
- Formatting items under Args/Returns as list items (-) with inline code (` `)
for the parameter/return type part.
- Preserving blank lines for paragraph breaks.
- Converting single newlines within paragraphs/list items to Markdown line breaks
(two spaces + newline) to ensure correct rendering.
Args:
docstring (str | None): The raw docstring string from a function's __doc__.
Returns:
str: The Markdown-formatted string suitable for gr.Markdown.
Returns a default message if the input docstring is None or empty.
"""
if not docstring:
return ""
# 1. Clean common indentation using inspect.cleandoc
# Split into lines afterwards
lines = inspect.cleandoc(docstring).splitlines()
formatted_lines = []
in_params_section = False # Flag to track if we are currently inside Args, Returns, Raises sections
summary_done = False
# 2. Process lines to add structural Markdown
for i, line in enumerate(lines):
stripped_line = line.strip()
if not stripped_line:
# Preserve blank lines as is - they become paragraph breaks (\n\n after join)
formatted_lines.append("")
in_params_section = False # Blank line often signifies the end of a section
continue
if not summary_done:
# The very first non-empty, non-indented line (after cleandoc) is the summary
formatted_lines.append(f"## {stripped_line}")
summary_done = True
continue
# Check for standard section headers (Args:, Returns:, Raises:, etc.)
# Use regex to be flexible with spacing and optional colons
# Added more common section names
section_match = re.match(r"^(Args|Arguments|Params|Parameters|Returns|Return|Raises|Raise|Example|Examples|Note|Notes|Warning|Warnings|Todo|Todos):?\s*", stripped_line, re.IGNORECASE)
if section_match:
section_header = section_match.group(0).strip() # Get the matched part (e.g., "Args:")
section_name = section_match.group(1) # Get the section type (e.g., "Args")
# Format the header in bold, remove trailing colon before bolding if present
header_text_bold = f"**{section_header.rstrip(':')}**:"
formatted_lines.append(header_text_bold)
# Set section flag for parameter/return sections
if section_name.lower() in ['args', 'arguments', 'params', 'parameters', 'returns', 'return', 'raises', 'raise']:
in_params_section = True
else:
in_params_section = False # For other sections like Examples, Notes, etc.
elif in_params_section and ':' in line:
# If we are in a parameter/return section and the line contains a colon,
# it's likely a parameter/return item in "name (type): description" format
parts = line.split(':', 1)
name_type_part = parts[0].strip()
description_part = parts[1].strip() if len(parts) > 1 else ""
# Format as a list item (-) with inline code (`) for the name/type part
# and regular text for the description.
formatted_line = f"- `{name_type_part}` : {description_part}"
formatted_lines.append(formatted_line)
elif in_params_section and ':' not in line and stripped_line:
# This might be a continuation line for the description of a parameter/return.
# Just add it, it will be handled by the single newline replacement later.
# Adding some indentation can improve readability in the raw markdown string,
# but the final rendering depends on the markdown renderer. Let's just add it as is.
formatted_lines.append(line) # Use original line before strip for potential original indentation
else:
# Other lines are regular text after the summary or in unhandled sections.
# Add them as is.
formatted_lines.append(line)
# 3. Join the processed lines back into a single string
joined_docstring = "\n".join(formatted_lines)
# 4. Convert single newlines to Markdown line breaks (two spaces + newline)
# This is crucial for text within paragraphs and multi-line list items to break correctly.
# Use the regex: look for a newline (\n) that is NOT preceded by another newline (?<!\n)
# and NOT followed by another newline (?!\n). Replace it with two spaces and a newline.
processed_docstring = re.sub(r'(?<!\n)\n(?!\n)', ' \n', joined_docstring)
# Handle cases where the cleandoc() might leave leading/trailing blank lines
# after processing. Markdown handles these well, but we can strip if desired.
# Keeping them might be fine as it aligns with standard Markdown paragraph separation.
# Let's strip outer whitespace just in case.
processed_docstring = processed_docstring.strip()
return processed_docstring