Skip to content

woodglue.apps.llm_docs

LLM-friendly documentation generation and Tornado handlers.

LLM-friendly documentation generation for woodglue namespaces.

Generates three artifact types: - llms.txt: index of all methods with one-line teasers - Per-method markdown: full docs with parameters, return types, referenced models - OpenAPI 3.0.3 spec: standard API spec

LlmsTxtHandler

Bases: _AuthDocHandler

GET /docs/llms.txt

Source code in src/woodglue/apps/llm_docs.py
class LlmsTxtHandler(_AuthDocHandler):
    """GET /docs/llms.txt"""

    @override
    def get(self) -> None:
        method_index: dict[str, dict[str, NamespaceNode]] = self.application.settings[
            "method_index"
        ]
        self.set_header("Content-Type", "text/plain; charset=utf-8")
        self.write(generate_llms_txt(method_index))

MethodDocHandler

Bases: _AuthDocHandler

GET /docs/methods/{prefix}.{module}/{name}.md

URL uses '/' in place of ':' for cleaner paths. The handler reverses this to look up the method by its nsref (which uses ':').

Source code in src/woodglue/apps/llm_docs.py
class MethodDocHandler(_AuthDocHandler):
    """GET /docs/methods/{prefix}.{module}/{name}.md

    URL uses '/' in place of ':' for cleaner paths. The handler reverses
    this to look up the method by its nsref (which uses ':').
    """

    @override
    def get(self, path: str) -> None:
        method_index: dict[str, dict[str, NamespaceNode]] = self.application.settings[
            "method_index"
        ]

        if not path.endswith(".md"):
            raise tornado.web.HTTPError(404)
        # Reverse the URL encoding: '/' back to ':'
        name = path[:-3].replace("/", ":")

        dot_pos = name.find(".")
        if dot_pos < 0:
            raise tornado.web.HTTPError(404)

        prefix = name[:dot_pos]
        method_name = name[dot_pos + 1 :]

        methods = method_index.get(prefix)
        if methods is None:
            raise tornado.web.HTTPError(404)

        node = methods.get(method_name)
        if node is None:
            raise tornado.web.HTTPError(404)

        md = generate_method_markdown(prefix, method_name, node)
        self.set_header("Content-Type", "text/markdown; charset=utf-8")
        self.write(md)

OpenApiHandler

Bases: _AuthDocHandler

GET /docs/openapi.json

Source code in src/woodglue/apps/llm_docs.py
class OpenApiHandler(_AuthDocHandler):
    """GET /docs/openapi.json"""

    @override
    def get(self) -> None:
        method_index: dict[str, dict[str, NamespaceNode]] = self.application.settings[
            "method_index"
        ]
        self.set_header("Content-Type", "application/json")
        self.write(generate_openapi_spec(method_index))

walk_namespace(ns)

Return (nsref, node) pairs for every NamespaceNode in ns.

Source code in src/woodglue/apps/llm_docs.py
def walk_namespace(ns: Namespace) -> list[tuple[str, NamespaceNode]]:
    """Return `(nsref, node)` pairs for every NamespaceNode in `ns`."""
    return list(ns._nodes.items())  # pyright: ignore[reportPrivateUsage]

build_method_index(namespaces)

Build a flat lookup: {prefix: {leaf_name: node}}.

Only includes methods tagged with "api" from namespaces where expose_api is True. This resolves the nested namespace structure into a simple two-level dict suitable for both RPC dispatch and documentation generation.

Source code in src/woodglue/apps/llm_docs.py
def build_method_index(
    namespaces: dict[str, tuple[Namespace, NamespaceEntry]],
) -> dict[str, dict[str, NamespaceNode]]:
    """
    Build a flat lookup: `{prefix: {leaf_name: node}}`.

    Only includes methods tagged with `"api"` from namespaces where
    `expose_api` is True. This resolves the nested namespace structure
    into a simple two-level dict suitable for both RPC dispatch and
    documentation generation.
    """
    index: dict[str, dict[str, NamespaceNode]] = {}
    for prefix, (ns, entry) in namespaces.items():
        if not entry.expose_api:
            continue
        methods: dict[str, NamespaceNode] = {}
        for leaf_name, node in walk_namespace(ns):
            if API_TAG in node.tags:
                methods[leaf_name] = node
        if methods:
            index[prefix] = methods
    return index

generate_llms_txt(method_index)

Generate an llms.txt index listing all methods in the method index.

Source code in src/woodglue/apps/llm_docs.py
def generate_llms_txt(method_index: dict[str, dict[str, NamespaceNode]]) -> str:
    """
    Generate an `llms.txt` index listing all methods in the method index.
    """
    lines = [
        "# Woodglue API",
        "",
        "> JSON-RPC 2.0 server",
        "",
        "## Methods",
        "",
    ]

    for prefix in sorted(method_index):
        for leaf_name, node in sorted(method_index[prefix].items()):
            teaser = _docstring_teaser(node.method.doc)
            qualified = f"{prefix}.{leaf_name}"
            # Replace ':' with '/' in doc URLs for cleaner paths
            doc_path = qualified.replace(":", "/")
            doc_link = f"/docs/methods/{doc_path}.md"
            if teaser:
                lines.append(f"- [{qualified}]({doc_link}): {teaser}")
            else:
                lines.append(f"- [{qualified}]({doc_link})")
    lines.append("")
    return "\n".join(lines)

generate_method_markdown(prefix, method_name, node)

Generate full markdown documentation for a single method.

Source code in src/woodglue/apps/llm_docs.py
def generate_method_markdown(prefix: str, method_name: str, node: NamespaceNode) -> str:
    """
    Generate full markdown documentation for a single method.
    """
    method = node.method
    resolved = _resolve_annotations(method)
    qualified = f"{prefix}.{method_name}"

    doc = method.doc or ""
    full_doc = doc.strip() if doc else ""

    lines = [
        f"# {qualified}",
        "",
        full_doc,
        "",
        "## Parameters",
        "",
        "| Name | Type | Required | Description |",
        "|------|------|----------|-------------|",
    ]

    for arg in method.args:
        ann = resolved.get(arg.name, arg.annotation)
        atype = _type_display(ann)
        required = "no" if arg.is_optional else "yes"
        desc = arg.description or "-"
        lines.append(f"| {arg.name} | {atype} | {required} | {desc} |")

    # Return type
    ret = resolved.get("return", method.return_annotation)
    if ret is not None and ret is not inspect.Parameter.empty:
        lines.extend(["", "## Returns", "", f"`{_type_display(ret)}`"])

    # Referenced models
    models = _collect_referenced_models(method)
    if models:
        lines.extend(["", "## Referenced Models", ""])
        for model in models:
            lines.append(_render_model_table(model))
            lines.append("")

    lines.append("")
    return "\n".join(lines)

generate_openapi_spec(method_index)

Build an OpenAPI 3.0.3 spec dict from the method index.

Source code in src/woodglue/apps/llm_docs.py
def generate_openapi_spec(
    method_index: dict[str, dict[str, NamespaceNode]],
) -> dict[str, Any]:
    """Build an OpenAPI 3.0.3 spec dict from the method index."""
    paths: dict[str, Any] = {}

    for prefix in sorted(method_index):
        for leaf_name, node in sorted(method_index[prefix].items()):
            method = node.method
            qualified = f"{prefix}.{leaf_name}"
            path = f"/rpc/{qualified}"

            properties: dict[str, Any] = {}
            required: list[str] = []
            for arg in method.args:
                prop = _python_type_to_schema(arg.annotation)
                if arg.description:
                    prop["description"] = arg.description
                if arg.default is not None and arg.default is not inspect.Parameter.empty:
                    prop["default"] = _json_safe_default(arg.default)
                properties[arg.name] = prop
                if not arg.is_optional:
                    required.append(arg.name)

            request_body_schema: dict[str, Any] = {
                "type": "object",
                "properties": properties,
            }
            if required:
                request_body_schema["required"] = required

            ret = method.return_annotation
            if ret is None or ret is inspect.Parameter.empty:
                response_schema: dict[str, Any] = {"type": "object"}
            else:
                response_schema = _python_type_to_schema(ret)

            summary = _docstring_teaser(method.doc)
            operation: dict[str, Any] = {
                "summary": summary or qualified,
                "operationId": qualified,
                "requestBody": {
                    "required": bool(required),
                    "content": {
                        "application/json": {
                            "schema": request_body_schema,
                        }
                    },
                },
                "responses": {
                    "200": {
                        "description": "Successful response",
                        "content": {
                            "application/json": {
                                "schema": response_schema,
                            }
                        },
                    }
                },
            }
            if method.doc:
                operation["description"] = method.doc.strip()

            paths[path] = {"post": operation}

    return {
        "openapi": "3.0.3",
        "info": {
            "title": "Woodglue JSON-RPC API",
            "version": "1.0.0",
        },
        "paths": paths,
    }