Ctrl K
ComunidadGuías…

scripts/build_offline_skill_prompt.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Concatenate the Logfire skills into one self-contained offline prompt.

The skills are designed to be fetched individually and on demand -- a hub
skill points at `logfire-instrumentation` by name, and each of those points
at `./references/*.md` files alongside it. That's the right shape for an
agent with live URL access. It's the wrong shape for a "copy this prompt"
button aimed at an agent that can't fetch anything: every "see the
logfire-infrastructure skill" or "see ./references/collector/..." becomes a
dead pointer.

This script produces the other artifact: one markdown file with everything
inlined, so a pointer like "the logfire-infrastructure skill" can be read as
"the section below headed accordingly" instead of a fetch that will fail.

Usage:
    uv run --no-project python scripts/build_offline_skill_prompt.py [-o OUTPUT]

Prints a word/char count to stderr either way; writes to OUTPUT (default
stdout) so callers can pipe it, diff it against a checked-in copy, or wire it
into a build step without this script knowing which.
"""

from __future__ import annotations

import argparse
import posixpath
import re
import sys
from pathlib import Path, PurePosixPath

REPO_ROOT = Path(__file__).resolve().parent.parent
SKILLS_ROOT = REPO_ROOT / 'logfire-sdk' / 'logfire' / '.agents' / 'skills'
PUBLIC_SKILLS_ROOT = 'https://pydantic.dev/.well-known/agent-skills'
AUTH_REFERENCE_PATH = 'logfire-instrumentation/references/auth.md'
AUTH_REFERENCE_ANCHOR = '#authenticate-and-select-the-exact-project'

# Hub first, then the three it routes to, in the order a reader would want
# to meet them: detect/install first, the two optional add-ons after.
SKILL_ORDER = [
    'logfire-setup',
    'logfire-instrumentation',
    'logfire-infrastructure',
    'logfire-evals',
]

# Every skill body links to this one, by relative path, from its very first
# step -- it is not an optional deep-dive, it is the auth command sequence
# every skill's Step 1 depends on. `--no-references` is meant to cut the
# language-specific deep dives (roughly half the bundle's size), not leave
# the one reference every skill needs unreachable: a `--no-references` build
# used to keep the "[Authenticate...](.../references/auth.md)" links from
# every skill while omitting the file they point at, so the one artifact
# whose whole point is "works with no fetching" sent a reader straight into
# a dead link for the most safety-critical reference in the bundle.
MANDATORY_REFERENCES = [
    'logfire-instrumentation/references/auth.md',
]
RELATIVE_MARKDOWN_LINK = re.compile(
    r'(?P<prefix>\]\()(?P<target>\.\.?/[^)#\s]+\.md)(?P<fragment>#[^)\s]+)?(?P<suffix>\))'
)
MARKDOWN_HEADING = re.compile(r'^(?P<marks>#{1,6})\s+(?P<title>.+)$')
MARKDOWN_FENCE = re.compile(r'^(?P<marker>`{3,}|~{3,})')


class BuildOfflinePromptError(RuntimeError):
    """A skill file was missing or malformed."""


def _strip_frontmatter(text: str) -> tuple[str, str]:
    """Split a SKILL.md into (description, body). The body keeps its own `#` title."""
    if not text.startswith('---\n'):
        raise BuildOfflinePromptError('SKILL.md is missing its frontmatter block')
    end = text.find('\n---\n', 4)
    if end == -1:
        raise BuildOfflinePromptError('SKILL.md frontmatter is not closed with a second `---`')
    frontmatter, body = text[4:end], text[end + 5 :]
    description = ''
    for line in frontmatter.splitlines():
        if line.startswith('description:'):
            description = line[len('description:') :].strip()
            break
    return description, body.strip('\n')


def _reference_files(skill_dir: Path) -> list[Path]:
    references_dir = skill_dir / 'references'
    if not references_dir.is_dir():
        return []
    return sorted(references_dir.rglob('*.md'))


def _reference_anchor(relative: str) -> str:
    """Return the stable HTML anchor for one inlined reference file."""
    slug = re.sub(r'[^a-z0-9]+', '-', relative.lower()).strip('-')
    return f'reference-{slug}'


def _reference_section_anchor(relative: str, fragment: str) -> str:
    """Qualify a source heading fragment with its inlined reference file."""
    return f'{_reference_anchor(relative)}--{fragment.removeprefix("#")}'


def _markdown_heading_slug(title: str) -> str:
    """Approximate GitHub's Markdown heading fragment for reference source headings."""
    without_links = re.sub(r'\[([^]]+)]\([^)]+\)', r'\1', title)
    return ''.join(
        character for character in without_links.casefold() if character.isalnum() or character in ' _-'
    ).replace(' ', '-')


def _qualify_reference_headings(text: str, relative: str) -> str:
    """Add collision-free aliases for headings inside one flattened reference file."""
    occurrences: dict[str, int] = {}
    fence: tuple[str, int] | None = None
    rendered: list[str] = []
    for line in text.splitlines(keepends=True):
        fence_match = MARKDOWN_FENCE.match(line.lstrip())
        if fence_match:
            marker = fence_match.group('marker')
            if fence is None:
                fence = marker[0], len(marker)
            elif marker[0] == fence[0] and len(marker) >= fence[1] and not line.lstrip()[len(marker) :].strip():
                fence = None
        elif fence is None and (heading := MARKDOWN_HEADING.fullmatch(line.rstrip('\r\n'))):
            slug = _markdown_heading_slug(heading.group('title'))
            occurrence = occurrences.get(slug, 0)
            occurrences[slug] = occurrence + 1
            fragment = slug if occurrence == 0 else f'{slug}-{occurrence}'
            anchor = _reference_section_anchor(relative, fragment)
            rendered.append(f'<a id="{anchor}"></a>\n\n')
        rendered.append(line)
    return ''.join(rendered)


def _rewrite_public_links_for_offline_bundle(
    text: str,
    *,
    source_path: PurePosixPath | None = None,
    bundled_references: frozenset[str] = frozenset(),
) -> str:
    """Point links to bundled skills and references at their inlined sections."""
    auth_sources = (
        f'{PUBLIC_SKILLS_ROOT}/{AUTH_REFERENCE_PATH}',
        f'../{AUTH_REFERENCE_PATH}',
        AUTH_REFERENCE_PATH,
        './references/auth.md',
    )
    for source in auth_sources:
        text = re.sub(
            rf'{re.escape(source)}(#[^)\s]+)?',
            lambda match: match.group(1) or AUTH_REFERENCE_ANCHOR,
            text,
        )
    for name in SKILL_ORDER:
        text = text.replace(f'{PUBLIC_SKILLS_ROOT}/{name}/SKILL.md', f'#skill-{name}')
        text = text.replace(f'{PUBLIC_SKILLS_ROOT}/{name}/', f'../{name}/')

    if source_path is not None:

        def replace_relative_link(match: re.Match[str]) -> str:
            target = match.group('target')
            resolved = posixpath.normpath((source_path.parent / target).as_posix())
            if resolved not in bundled_references:
                return match.group(0)
            fragment = match.group('fragment')
            destination = (
                f'#{_reference_section_anchor(resolved, fragment)}' if fragment else f'#{_reference_anchor(resolved)}'
            )
            return f'{match.group("prefix")}{destination}{match.group("suffix")}'

        text = RELATIVE_MARKDOWN_LINK.sub(replace_relative_link, text)
    return text


def _render_skill(name: str, *, bundled_references: frozenset[str]) -> str:
    skill_dir = SKILLS_ROOT / name
    skill_md = skill_dir / 'SKILL.md'
    if not skill_md.is_file():
        raise BuildOfflinePromptError(f'{skill_md} does not exist')
    description, body = _strip_frontmatter(skill_md.read_text(encoding='utf-8'))
    section = [f'# Skill: {name}', '']
    if description:
        section += [f'*{description}*', '']
    section.append(
        _rewrite_public_links_for_offline_bundle(
            body,
            source_path=PurePosixPath(name) / 'SKILL.md',
            bundled_references=bundled_references,
        )
    )
    return '\n'.join(section)


def _render_appendix(name: str, *, bundled_references: frozenset[str]) -> str:
    skill_dir = SKILLS_ROOT / name
    parts: list[str] = []
    for ref in _reference_files(skill_dir):
        relative = ref.relative_to(SKILLS_ROOT)
        relative_path = relative.as_posix()
        if relative_path not in bundled_references:
            continue
        content = _rewrite_public_links_for_offline_bundle(
            ref.read_text(encoding='utf-8').strip(),
            source_path=PurePosixPath(relative_path),
            bundled_references=bundled_references,
        )
        content = _qualify_reference_headings(content, relative_path)
        parts.append(f'<a id="{_reference_anchor(relative_path)}"></a>\n\n## {relative_path}\n\n{content}')
    return '\n\n'.join(parts)


def _preamble(*, include_references: bool) -> str:
    references_note = ' Authentication links jump directly to the inlined authentication appendix.'
    references_note += (
        ' Links to the other bundled reference files jump to their inlined entries in the '
        '**Reference Files** appendix at the end.'
        if include_references
        else ' This build omits the other `./references/...` deep-dive files (language-specific '
        'edge cases) to stay shorter; if one turns out to matter, fetch it directly from the repo.'
    )
    return (
        '# Pydantic Logfire — Offline Setup Prompt\n\n'
        'This bundle includes the `logfire-setup` hub and its SDK specialists:\n'
        '`logfire-instrumentation`, `logfire-infrastructure`, and `logfire-evals`.\n'
        'Their bundled references resolve without a network fetch.\n\n'
        'Frontend, Gateway, coding-agent, and MCP query/UI specialists are not bundled.\n'
        'If fetching them is unavailable, report that limitation; do not substitute SDK setup.\n\n'
        'A pointer to "the `logfire-infrastructure` skill" (or any other skill named\n'
        'above) means the section below headed `# Skill: logfire-infrastructure` --\n'
        f'read it in place of fetching it.{references_note}\n'
    )


def build(*, include_references: bool = True) -> str:
    """Concatenate every skill in `SKILL_ORDER`, optionally with their reference files.

    Even when `include_references` is False, MANDATORY_REFERENCES are still inlined --
    see that constant for why omitting them isn't just "shorter", it's a dead link.
    """
    bundled_references = frozenset(
        ref.relative_to(SKILLS_ROOT).as_posix()
        for name in SKILL_ORDER
        for ref in _reference_files(SKILLS_ROOT / name)
        if include_references or ref.relative_to(SKILLS_ROOT).as_posix() in MANDATORY_REFERENCES
    )
    skill_sections = [_render_skill(name, bundled_references=bundled_references) for name in SKILL_ORDER]
    parts = [_preamble(include_references=include_references), *skill_sections]
    appendix_sections = [
        rendered for name in SKILL_ORDER if (rendered := _render_appendix(name, bundled_references=bundled_references))
    ]
    if appendix_sections:
        parts.append('# Reference Files\n\n' + '\n\n---\n\n'.join(appendix_sections))
    return '\n\n---\n\n'.join(parts) + '\n'


def main() -> None:
    """CLI entry point: build the prompt and write it to `--output` or stdout."""
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('-o', '--output', type=Path, default=None, help='write to this path instead of stdout')
    parser.add_argument(
        '--no-references',
        action='store_true',
        help=(
            'skill bodies only -- skip the deep-dive reference-file appendix (roughly half the '
            'size), except the mandatory auth reference every skill depends on, which is always '
            'kept'
        ),
    )
    args = parser.parse_args()

    prompt = build(include_references=not args.no_references)
    word_count = len(prompt.split())
    print(
        f'{word_count} words, {len(prompt)} chars (~{len(prompt) // 4} tokens by the usual chars/4 estimate)',
        file=sys.stderr,
    )

    if args.output:
        args.output.write_text(prompt, encoding='utf-8')
        print(f'wrote {args.output}', file=sys.stderr)
    else:
        sys.stdout.write(prompt)


if __name__ == '__main__':
    main()