From 3306747b342118ea886e3e1d41f13c06ab7e8912 Mon Sep 17 00:00:00 2001 From: Dmitrii Galkin Date: Wed, 29 Jul 2026 00:26:17 +0400 Subject: [PATCH] Inline body field schema and realistic example payloads into SKILL.md SKILL.md previously showed a generic --body '{"key": "value"}' for every endpoint with a request body, regardless of its actual schema. An agent reading only SKILL.md had to run --help first to discover the real field names, types, required-ness, and enums. Reuses cli_generator's _body_schema_help() to list each command's real body fields under it, and generates a realistic example payload (using schema defaults/enums where available) instead of the generic placeholder in the Examples section. Closes #10 --- .../generators/skill_generator.py | 42 +++++++++++++++++++ fastapi_to_skill/templates/skill.md.jinja2 | 10 ++++- tests/test_e2e.py | 34 +++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/fastapi_to_skill/generators/skill_generator.py b/fastapi_to_skill/generators/skill_generator.py index cd2168b..be9ee0c 100644 --- a/fastapi_to_skill/generators/skill_generator.py +++ b/fastapi_to_skill/generators/skill_generator.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import re from collections import defaultdict from pathlib import Path @@ -7,6 +8,7 @@ from jinja2 import Environment, FileSystemLoader from ..models import APISpec +from .cli_generator import _body_schema_help TEMPLATES_DIR = Path(__file__).parent.parent / "templates" TEMPLATE_NAME = "skill.md.jinja2" @@ -39,6 +41,44 @@ def _group_by_tag(spec: APISpec) -> dict[str, list]: return dict(groups) +def _body_schema_help_md(schema: dict) -> str: + """Render body schema help with real newlines, for embedding in Markdown + (the cli_generator version uses "\\n" so it survives as a Python docstring).""" + return _body_schema_help(schema).replace("\\n", "\n") + + +def _example_value(field: dict): + """Produce a realistic example value for a resolved JSON Schema field.""" + if "example" in field: + return field["example"] + if "default" in field: + return field["default"] + if "enum" in field: + return field["enum"][0] + ftype = field.get("type", "string") + if ftype == "object" and "properties" in field: + return _example_payload(field) + if ftype == "array": + return [_example_value(field.get("items", {}))] + return { + "string": "string", + "integer": 0, + "number": 0.0, + "boolean": True, + }.get(ftype, "value") + + +def _example_payload(schema: dict) -> dict: + """Build a realistic example request-body payload from a resolved schema.""" + return {name: _example_value(field) for name, field in schema.get("properties", {}).items()} + + +def _example_payload_json(schema: dict) -> str: + if not schema.get("properties"): + return '{"key": "value"}' + return json.dumps(_example_payload(schema)) + + def _get_env_vars(spec: APISpec, env_prefix: str) -> list[str]: """Collect all environment variables the generated CLI uses.""" env_vars = [f"{env_prefix}_BASE_URL"] @@ -61,6 +101,8 @@ def generate_skill(spec: APISpec, output_dir: Path) -> Path: env.filters["slugify"] = _slugify env.filters["oneline"] = _oneline env.filters["truncate"] = _truncate + env.filters["body_schema_help"] = _body_schema_help_md + env.filters["example_payload_json"] = _example_payload_json template = env.get_template(TEMPLATE_NAME) diff --git a/fastapi_to_skill/templates/skill.md.jinja2 b/fastapi_to_skill/templates/skill.md.jinja2 index d5ba89d..6dc2b52 100644 --- a/fastapi_to_skill/templates/skill.md.jinja2 +++ b/fastapi_to_skill/templates/skill.md.jinja2 @@ -65,6 +65,14 @@ python ${CLAUDE_SKILL_DIR}/cli.py search "" {%- for p in ep.parameters if p.location == "query" %} `--{{ p.name | replace('_', '-') }}`{% endfor %} {%- if ep.request_body %} `--body '{...}'`{% endif %} +{% if ep.request_body and ep.request_body.schema_dict %} + ``` +{% for line in (ep.request_body.schema_dict | body_schema_help).split('\n') %} + {{ line }} +{% endfor %} + ``` +{% endif %} + {% endfor %} {% endfor %} @@ -72,7 +80,7 @@ python ${CLAUDE_SKILL_DIR}/cli.py search "" {% for ep in spec.endpoints[:5] %} ```bash -python ${CLAUDE_SKILL_DIR}/cli.py {{ ep.operation_id | replace('_', '-') }}{% for p in ep.parameters if p.location == "path" %} <{{ p.name }}>{% endfor %}{% for p in ep.parameters if p.location == "query" %} --{{ p.name | replace('_', '-') }} value{% endfor %}{% if ep.request_body %} --body '{"key": "value"}'{% endif %} +python ${CLAUDE_SKILL_DIR}/cli.py {{ ep.operation_id | replace('_', '-') }}{% for p in ep.parameters if p.location == "path" %} <{{ p.name }}>{% endfor %}{% for p in ep.parameters if p.location == "query" %} --{{ p.name | replace('_', '-') }} value{% endfor %}{% if ep.request_body %} --body '{{ ep.request_body.schema_dict | example_payload_json }}'{% endif %} ``` {% endfor %} diff --git a/tests/test_e2e.py b/tests/test_e2e.py index 0e4b51f..72eb33a 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -175,3 +175,37 @@ def test_complex_full_pipeline(complex_spec, tmp_path): assert cli_path.exists() assert skill_path.exists() ast.parse(cli_path.read_text()) + + +# --- SKILL.md body schema inlining --- + + +def test_skill_md_inlines_body_field_schema(complex_spec, tmp_path): + """SKILL.md should show the real body fields, not just a generic --body placeholder.""" + api = parse_spec(complex_spec) + skill_path = generate_skill(api, tmp_path) + content = skill_path.read_text() + assert "Body fields:" in content + assert "name: string (required)" in content + assert "email: string (required)" in content + assert "role: enum('admin', 'customer', 'seller')" in content + # No literal "\n" escape sequences should leak into the Markdown + assert "\\n" not in content + + +def test_skill_md_realistic_example_payload(complex_spec, tmp_path): + """The --body example should use real field names/values, not {"key": "value"}.""" + api = parse_spec(complex_spec) + skill_path = generate_skill(api, tmp_path) + content = skill_path.read_text() + assert '{"key": "value"}' not in content + assert '"name": "string"' in content + assert '"email": "string"' in content + assert '"role": "customer"' in content + + +def test_skill_md_example_payload_defaults_for_no_properties(petstore_spec, tmp_path): + """Endpoints without a body keep no --body at all; this just guards the fallback.""" + from fastapi_to_skill.generators.skill_generator import _example_payload_json + + assert _example_payload_json({}) == '{"key": "value"}'