# 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 (?