Add Swagger UI defaults and enable JSON file watching in Docker

- Updated all API endpoint parameters to use `example` instead of `examples` for better Swagger UI experience
- Parameters now pre-fill with reasonable defaults when testing endpoints in /api/docs
- Added --reload and --reload-include '*.json' flags to docker-compose for auto-reload on JSON changes
- Included commentary-generator agent configuration for AI-powered verse commentary

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-29 13:37:04 -05:00
parent 2776aa984a
commit 94a8801251
3 changed files with 42 additions and 34 deletions
+8
View File
@@ -0,0 +1,8 @@
---
name: commentary-generator
description: when asked to generate verse commentary
model: opus
color: blue
---
Write a thoughtful theological commentary on the given KJV bible verse, and add it to verse_commentary.json when complete. Follow the schema correctly.
+1 -1
View File
@@ -10,4 +10,4 @@ services:
- PYTHONUNBUFFERED=1
- PRELOAD_INTERLINEAR=true
restart: unless-stopped
command: uv run uvicorn kjvstudy_org.server:app --host 0.0.0.0 --port 8000
command: uv run uvicorn kjvstudy_org.server:app --host 0.0.0.0 --port 8000 --reload --reload-include '*.json'
+33 -33
View File
@@ -244,8 +244,8 @@ def api_health_check():
@router.get("/search")
def search_api(
q: str = Query(..., description="Search query", examples=["faith"]),
limit: Optional[int] = Query(None, description="Max results", examples=[10])
q: str = Query(..., description="Search query", example="faith"),
limit: Optional[int] = Query(None, description="Max results", example=10)
):
"""JSON API endpoint for search."""
if not q or len(q.strip()) < 2:
@@ -268,8 +268,8 @@ def search_api(
@router.get("/universal-search")
def universal_search_api(
q: str = Query(..., description="Search query", examples=["love"]),
limit: int = Query(5, description="Max results per category", examples=[5])
q: str = Query(..., description="Search query", example="love"),
limit: int = Query(5, description="Max results per category", example=5)
):
"""Universal search across all content types."""
if not q or len(q.strip()) < 2:
@@ -519,9 +519,9 @@ def verse_of_the_day_api():
}
)
def api_get_verse(
book: str = Path(..., description="Book name (supports abbreviations)", examples=["John", "Gen", "Mt"]),
chapter: int = Path(..., description="Chapter number", examples=[3], ge=1),
verse: int = Path(..., description="Verse number", examples=[16], ge=1)
book: str = Path(..., description="Book name (supports abbreviations)", example="John"),
chapter: int = Path(..., description="Chapter number", example=3, ge=1),
verse: int = Path(..., description="Verse number", example=16, ge=1)
):
"""Get a single verse with red letter information."""
canonical_name = normalize_book_name(book)
@@ -628,10 +628,10 @@ def api_get_verse(
}
)
def api_get_verse_range(
book: str = Path(..., description="Book name (supports abbreviations)", examples=["Psalms", "Mt"]),
chapter: int = Path(..., description="Chapter number", examples=[23], ge=1),
start: int = Path(..., description="Starting verse number", examples=[1], ge=1),
end: int = Path(..., description="Ending verse number", examples=[6], ge=1)
book: str = Path(..., description="Book name (supports abbreviations)", example="Psalms"),
chapter: int = Path(..., description="Chapter number", example=23, ge=1),
start: int = Path(..., description="Starting verse number", example=1, ge=1),
end: int = Path(..., description="Ending verse number", example=6, ge=1)
):
"""Get a range of verses with red letter information."""
canonical_name = normalize_book_name(book)
@@ -677,9 +677,9 @@ def api_get_verse_range(
@router.get("/interlinear/{book}/{chapter}/{verse}")
def api_get_interlinear(
book: str = Path(..., description="Book name", examples=["John"]),
chapter: int = Path(..., description="Chapter number", examples=[1]),
verse: int = Path(..., description="Verse number", examples=[1])
book: str = Path(..., description="Book name", example="John"),
chapter: int = Path(..., description="Chapter number", example=1),
verse: int = Path(..., description="Verse number", example=1)
):
"""Get interlinear (word-by-word) data for a verse."""
canonical_name = normalize_book_name(book)
@@ -761,7 +761,7 @@ def api_get_books():
@router.get("/books/{book}")
def api_get_book(book: str = Path(..., description="Book name", examples=["Genesis"])):
def api_get_book(book: str = Path(..., description="Book name", example="Genesis")):
"""Get details about a specific book including introduction and study material."""
canonical_name = normalize_book_name(book)
if canonical_name:
@@ -810,7 +810,7 @@ def api_get_book(book: str = Path(..., description="Book name", examples=["Genes
@router.get("/books/{book}/pdf")
async def api_book_pdf(book: str = Path(..., description="Book name", examples=["Genesis"])):
async def api_book_pdf(book: str = Path(..., description="Book name", example="Genesis")):
"""Generate PDF for an entire Bible book."""
if not WEASYPRINT_AVAILABLE:
raise HTTPException(
@@ -872,8 +872,8 @@ async def api_book_pdf(book: str = Path(..., description="Book name", examples=[
@router.get("/books/{book}/chapters/{chapter}")
def api_get_chapter(
book: str = Path(..., description="Book name", examples=["Romans"]),
chapter: int = Path(..., description="Chapter number", examples=[8])
book: str = Path(..., description="Book name", example="Romans"),
chapter: int = Path(..., description="Chapter number", example=8)
):
"""Get all verses in a chapter."""
canonical_name = normalize_book_name(book)
@@ -904,8 +904,8 @@ def api_get_chapter(
@router.get("/books/{book}/chapters/{chapter}/pdf")
async def api_chapter_pdf(
book: str = Path(..., description="Book name", examples=["Romans"]),
chapter: int = Path(..., description="Chapter number", examples=[8])
book: str = Path(..., description="Book name", example="Romans"),
chapter: int = Path(..., description="Chapter number", example=8)
):
"""Generate PDF for a specific Bible chapter."""
if not WEASYPRINT_AVAILABLE:
@@ -954,7 +954,7 @@ async def api_chapter_pdf(
@router.get("/books/{book}/text")
def api_get_book_text(book: str = Path(..., description="Book name", examples=["Philemon"])):
def api_get_book_text(book: str = Path(..., description="Book name", example="Philemon")):
"""Get all text content of a book."""
canonical_name = normalize_book_name(book)
if canonical_name:
@@ -1027,9 +1027,9 @@ def api_get_bible():
@router.get("/cross-references/{book}/{chapter}/{verse}")
def api_get_cross_references(
book: str = Path(..., description="Book name", examples=["John"]),
chapter: int = Path(..., description="Chapter number", examples=[3]),
verse: int = Path(..., description="Verse number", examples=[16])
book: str = Path(..., description="Book name", example="John"),
chapter: int = Path(..., description="Chapter number", example=3),
verse: int = Path(..., description="Verse number", example=16)
):
"""Get cross-references for a verse."""
canonical_name = normalize_book_name(book)
@@ -1072,7 +1072,7 @@ def api_get_topics():
@router.get("/topics/{topic_name}")
def api_get_topic(topic_name: str = Path(..., description="Topic name", examples=["faith"])):
def api_get_topic(topic_name: str = Path(..., description="Topic name", example="faith")):
"""Get details about a specific topic."""
topic = get_topic(topic_name)
if not topic:
@@ -1098,7 +1098,7 @@ def api_get_reading_plans():
@router.get("/reading-plans/{plan_id}")
def api_get_reading_plan(plan_id: str = Path(..., description="Reading plan ID", examples=["chronological"])):
def api_get_reading_plan(plan_id: str = Path(..., description="Reading plan ID", example="chronological")):
"""Get details about a specific reading plan."""
plan = get_plan(plan_id)
if not plan:
@@ -1146,7 +1146,7 @@ def api_get_stories():
@router.get("/stories/{slug}")
def api_get_story(slug: str = Path(..., description="Story slug", examples=["creation-of-the-world"])):
def api_get_story(slug: str = Path(..., description="Story slug", example="creation-of-the-world")):
"""Get a specific Bible story by slug."""
story = get_story_by_slug(slug)
if not story:
@@ -1318,7 +1318,7 @@ def api_list_resource_categories():
}
)
def api_get_resource_category(
category: str = Path(..., description="Resource category name", examples=["biblical_locations", "angels", "prophets"])
category: str = Path(..., description="Resource category name", example="biblical_locations")
):
"""Get all items in a specific resource category."""
if category not in RESOURCES_DATA:
@@ -1392,8 +1392,8 @@ def api_get_resource_category(
}
)
def api_get_resource_item(
category: str = Path(..., description="Resource category name", examples=["biblical_locations"]),
slug: str = Path(..., description="Resource item slug", examples=["garden-of-eden"])
category: str = Path(..., description="Resource category name", example="biblical_locations"),
slug: str = Path(..., description="Resource item slug", example="garden-of-eden")
):
"""Get detailed information about a specific resource item."""
if category not in CATEGORY_TO_DATA:
@@ -1434,7 +1434,7 @@ def api_get_resource_item(
@router.get("/resources/{category}/pdf")
async def api_get_resource_category_pdf(
category: str = Path(..., description="Resource category name", examples=["biblical_locations"])
category: str = Path(..., description="Resource category name", example="biblical_locations")
):
"""Generate PDF for an entire resource category."""
# Check if category exists first (before checking WeasyPrint)
@@ -1479,8 +1479,8 @@ async def api_get_resource_category_pdf(
@router.get("/resources/{category}/{slug}/pdf")
async def api_get_resource_item_pdf(
category: str = Path(..., description="Resource category name", examples=["biblical_locations"]),
slug: str = Path(..., description="Resource item slug", examples=["garden-of-eden"])
category: str = Path(..., description="Resource category name", example="biblical_locations"),
slug: str = Path(..., description="Resource item slug", example="garden-of-eden")
):
"""Generate PDF for a specific resource item."""
# Check if category exists first (before checking WeasyPrint)