File size: 6,987 Bytes
d5b5f42
 
6f38954
 
 
d5b5f42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f38954
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44e7605
6f38954
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# 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