agentsclimarketplace

Document converter priority registry

Skill kjuhwa/skills-hub/skills/architecture/document-converter-priority-registry

Pluggable converter registry where each candidate declares a numeric priority, registrations are prepended, and dispatch re-sorts stably per call so the most recently registered converter at a priority wins.From its SKILL.md

Install
npx -y skills add kjuhwa/skills-hub --skill document-converter-priority-registry

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

5.6 KB, ~1.1k tokens by cl100k_base, as published. Nobody here has run it

Document Converter Priority Registry

Generic pluggable-dispatch pattern: a host owns an ordered list of (converter, priority) registrations and picks one per request by (a) sorting stably by priority, (b) walking the sorted list, (c) asking each one "can you handle this?" (accepts()), and (d) calling convert() on the first that says yes. New registrations are insert(0, ...) so, at equal priority, the most recently registered wins — giving late-loaded plugins a way to shadow a built-in without changing core code.

When to use

  • You have a polymorphic handler contract (parser, converter, serializer, router).
  • Built-in implementations must coexist with runtime-registered plugins.
  • The order of tries matters: specific beats generic, newer beats older at the same priority.
  • Priorities are fixed per converter but may change between calls (re-sort each call).

The shape

from dataclasses import dataclass
from typing import List

PRIORITY_SPECIFIC_FILE_FORMAT = 0.0   # specific: PDF, DOCX, Wikipedia
PRIORITY_GENERIC_FILE_FORMAT  = 10.0  # catch-all: text/*, html

@dataclass(kw_only=True, frozen=True)
class ConverterRegistration:
    converter: "DocumentConverter"
    priority: float

class Host:
    def __init__(self):
        self._converters: List[ConverterRegistration] = []

    def register_converter(self, converter, *, priority: float = PRIORITY_SPECIFIC_FILE_FORMAT):
        # insert at index 0 so later registrations win stable-sort ties
        self._converters.insert(0, ConverterRegistration(converter=converter, priority=priority))

    def dispatch(self, *args, **kwargs):
        # Re-sort on every call — priorities may change between calls.
        # `sorted` is stable → same-priority entries keep insertion order.
        for reg in sorted(self._converters, key=lambda r: r.priority):
            if reg.converter.accepts(*args, **kwargs):
                return reg.converter.convert(*args, **kwargs)

Why these three choices

  1. Lower priority value = tried first. Counter-intuitive, but useful: "specific format" = priority 0, "generic catch-all" = priority 10. New plugins can pick any float to slot themselves in. Document this loudly to avoid confusion.
  2. Insert at head + stable sort. Equal-priority registrations preserve insertion order, and the most-recent insertion lands at the front of the tie-group. Plugins can override built-ins without extra API.
  3. Re-sort per call. Priorities are rare to change, but cheap to re-sort (N is small). Avoids a stale-cache bug class if a converter exposes a mutable priority.

Plugin override in practice

Built-in registrations (all priority 0 except the three generic ones at 10):

self.register_converter(PlainTextConverter(), priority=10.0)   # generic
self.register_converter(HtmlConverter(),      priority=10.0)   # generic
self.register_converter(PdfConverter())                         # specific, p=0
self.register_converter(DocxConverter())                        # specific, p=0
# ...

A plugin then shadows the PDF handler by simply doing:

host.register_converter(MyPdfConverter())   # also priority 0 → now at head of tie group → wins

No introspection, no unregister step, no plugin lifecycle API.

Plugin loader glue

Load plugins via importlib.metadata.entry_points, wrap each in try/except so a broken plugin doesn't kill the host:

from importlib.metadata import entry_points
for ep in entry_points(group="markitdown.plugin"):
    try:
        plugin = ep.load()
        plugin.register_converters(host)
    except Exception:
        warn(f"Plugin '{ep.name}' failed to load ... skipping:\n{traceback.format_exc()}")

See companion skill python-entrypoint-plugin-loader for the full pattern.

Anti-patterns

  • Using a dict[str, Converter] by extension. Collapses plugin override, forces one-handler-per-extension, can't encode "try these in order."
  • Reversed priority (higher = first). You'll save ten minutes on read-intuition but lose hours when a plugin author picks the wrong sign and their handler never runs. Pick one convention and document it.
  • Single sorted list, no re-sort. Subtle bug: if any converter exposes a settable priority, your cached sort goes stale.
  • append() instead of insert(0). At equal priority you now favor the oldest registration, which is the opposite of what plugin authors expect.

Variations

  • Multi-hit dispatch. For "chain of responsibility where every responder must run," iterate without break — still uses the same sorted list.
  • Failover. If convert() raises a retriable error, record it and fall through to the next converter (markitdown does this; see companion skill accepts-then-convert-two-phase-dispatch).
  • Multiple guesses. If the request's signal (e.g., mimetype) is ambiguous, iterate over multiple input guesses in an outer loop and the sorted converter list in an inner loop.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,499. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.