rewrite ruby to python
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
John Lamb
2026-01-26 14:39:43 -06:00
parent a3cef61d5d
commit fedf2ff8e4
34 changed files with 1752 additions and 6574 deletions

View File

@@ -11,15 +11,15 @@
"plugins": [
{
"name": "compound-engineering",
"description": "AI-powered development tools that get smarter with every use. Make each unit of engineering work easier than the last. Includes 28 specialized agents, 24 commands, and 15 skills.",
"version": "2.28.0",
"description": "AI-powered development tools that get smarter with every use. Make each unit of engineering work easier than the last. Includes 27 specialized agents, 24 commands, and 15 skills.",
"version": "2.29.0",
"author": {
"name": "Kieran Klaassen",
"url": "https://github.com/kieranklaassen",
"email": "kieran@every.to"
},
"homepage": "https://github.com/EveryInc/compound-engineering-plugin",
"tags": ["ai-powered", "compound-engineering", "workflow-automation", "code-review", "quality", "knowledge-management", "image-generation"],
"tags": ["ai-powered", "compound-engineering", "workflow-automation", "code-review", "fastapi", "python", "knowledge-management"],
"source": "./plugins/compound-engineering"
},
{

View File

@@ -1,7 +1,7 @@
{
"name": "compound-engineering",
"version": "2.28.0",
"description": "AI-powered development tools. 28 agents, 24 commands, 15 skills, 1 MCP server for code review, research, design, and workflow automation.",
"version": "2.29.0",
"description": "AI-powered development tools. 27 agents, 24 commands, 15 skills, 1 MCP server for code review, research, design, and workflow automation.",
"author": {
"name": "Kieran Klaassen",
"email": "kieran@every.to",
@@ -15,8 +15,7 @@
"compound-engineering",
"workflow-automation",
"code-review",
"rails",
"ruby",
"fastapi",
"python",
"typescript",
"knowledge-management",

View File

@@ -5,6 +5,64 @@ All notable changes to the compound-engineering plugin will be documented in thi
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.29.0] - 2026-01-26
### Changed
- **Backend focus shift: Ruby/Rails → Python/FastAPI** - Comprehensive conversion of backend-focused components
- All backend-related agents and skills now target Python/FastAPI instead of Ruby/Rails
- TypeScript/React frontend components remain unchanged
### Added
- **`tiangolo-fastapi-reviewer` agent** - FastAPI code review from Sebastián Ramírez's perspective
- Reviews for Pydantic model patterns, async/await discipline, dependency injection
- Identifies Flask/Django patterns contaminating FastAPI codebases
- Enforces OpenAPI schema design and type hints
- **`python-package-readme-writer` agent** - Create concise READMEs for Python packages
- Follows modern Python packaging conventions (pyproject.toml, src layout)
- Targets PyPI publication with pip/uv/poetry installation instructions
- **`fastapi-style` skill** - Write FastAPI code following opinionated best practices
- Thin routers, rich Pydantic models with validation
- SQLAlchemy 2.0 async patterns, dependency injection
- References for routers, models, database, testing, security, background tasks
- **`python-package-writer` skill** - Write Python packages following production-ready patterns
- Modern packaging with pyproject.toml, src layout, pytest
- Zero or minimal dependencies philosophy
- Type hints, configuration patterns, FastAPI integration
- **`dspy-python` skill** - Build LLM applications with DSPy framework
- Signature-based prompting, teleprompter optimization
- FastAPI integration patterns, pytest testing
- Replaced dspy-ruby skill
- **Enhanced `kieran-python-reviewer` agent** - Now includes FastAPI-specific sections
- Pydantic model patterns, async/await discipline
- Dependency injection, OpenAPI schema design
- SQLAlchemy 2.0 async, router organization, security patterns
- **Updated `lint` agent** - Now targets Python files
- Uses ruff for linting/formatting, mypy for type checking
- bandit for security scanning, djlint for Jinja2 templates
### Removed
- **`dhh-rails-reviewer` agent** - Replaced by tiangolo-fastapi-reviewer
- **`kieran-rails-reviewer` agent** - Functionality merged into kieran-python-reviewer
- **`ankane-readme-writer` agent** - Replaced by python-package-readme-writer
- **`dhh-rails-style` skill** - Replaced by fastapi-style
- **`andrew-kane-gem-writer` skill** - Replaced by python-package-writer
- **`dspy-ruby` skill** - Replaced by dspy-python
### Summary
- 27 agents, 24 commands, 15 skills, 1 MCP server
---
## [2.28.0] - 2026-01-21
### Added

View File

@@ -7,15 +7,15 @@ AI-powered development tools that get smarter with every use. Make each unit of
| Component | Count |
|-----------|-------|
| Agents | 27 |
| Commands | 20 |
| Skills | 14 |
| Commands | 24 |
| Skills | 15 |
| MCP Servers | 1 |
## Agents
Agents are organized into categories for easier discovery.
### Review (14)
### Review (13)
| Agent | Description |
|-------|-------------|
@@ -25,22 +25,22 @@ Agents are organized into categories for easier discovery.
| `data-integrity-guardian` | Database migrations and data integrity |
| `data-migration-expert` | Validate ID mappings match production, check for swapped values |
| `deployment-verification-agent` | Create Go/No-Go deployment checklists for risky data changes |
| `dhh-rails-reviewer` | Rails review from DHH's perspective |
| `kieran-rails-reviewer` | Rails code review with strict conventions |
| `kieran-python-reviewer` | Python code review with strict conventions |
| `kieran-python-reviewer` | Python/FastAPI code review with strict conventions |
| `kieran-typescript-reviewer` | TypeScript code review with strict conventions |
| `tiangolo-fastapi-reviewer` | FastAPI review from Sebastián Ramírez's perspective |
| `pattern-recognition-specialist` | Analyze code for patterns and anti-patterns |
| `performance-oracle` | Performance analysis and optimization |
| `security-sentinel` | Security audits and vulnerability assessments |
| `julik-frontend-races-reviewer` | Review JavaScript/Stimulus code for race conditions |
### Research (4)
### Research (5)
| Agent | Description |
|-------|-------------|
| `best-practices-researcher` | Gather external best practices and examples |
| `framework-docs-researcher` | Research framework documentation and best practices |
| `git-history-analyzer` | Analyze git history and code evolution |
| `learnings-researcher` | Research and extract learnings from documentation |
| `repo-research-analyst` | Research repository structure and conventions |
### Design (3)
@@ -57,7 +57,7 @@ Agents are organized into categories for easier discovery.
|-------|-------------|
| `bug-reproduction-validator` | Systematically reproduce and validate bug reports |
| `every-style-editor` | Edit content to conform to Every's style guide |
| `lint` | Run linting and code quality checks on Ruby and ERB files |
| `lint` | Run linting and code quality checks on Python files |
| `pr-comment-resolver` | Address PR comments and implement fixes |
| `spec-flow-analyzer` | Analyze user flows and identify gaps in specifications |
@@ -65,7 +65,7 @@ Agents are organized into categories for easier discovery.
| Agent | Description |
|-------|-------------|
| `ankane-readme-writer` | Create READMEs following Ankane-style template for Ruby gems |
| `python-package-readme-writer` | Create concise READMEs for Python packages |
## Commands
@@ -85,21 +85,25 @@ Core workflow commands use `workflows:` prefix to avoid collisions with built-in
| Command | Description |
|---------|-------------|
| `/deepen-plan` | Enhance plans with parallel research agents for each section |
| `/agent-native-audit` | Run comprehensive agent-native architecture review with scored principles |
| `/changelog` | Create engaging changelogs for recent merges |
| `/create-agent-skill` | Create or edit Claude Code skills |
| `/deepen-plan` | Enhance plans with parallel research agents for each section |
| `/deploy-docs` | Validate and prepare documentation for GitHub Pages deployment |
| `/feature-video` | Record video walkthroughs and add to PR description |
| `/generate_command` | Generate new slash commands |
| `/heal-skill` | Fix skill documentation issues |
| `/lfg` | Full autonomous engineering workflow |
| `/plan_review` | Multi-agent plan review in parallel |
| `/release-docs` | Build and update the documentation site with current plugin components |
| `/report-bug` | Report a bug in the plugin |
| `/reproduce-bug` | Reproduce bugs using logs and console |
| `/resolve_parallel` | Resolve TODO comments in parallel |
| `/resolve_pr_parallel` | Resolve PR comments in parallel |
| `/resolve_todo_parallel` | Resolve todos in parallel |
| `/triage` | Triage and prioritize issues |
| `/test-browser` | Run browser tests on PR-affected pages |
| `/triage` | Triage and prioritize issues |
| `/xcode-test` | Build and test iOS apps on simulator |
| `/feature-video` | Record video walkthroughs and add to PR description |
## Skills
@@ -113,18 +117,19 @@ Core workflow commands use `workflows:` prefix to avoid collisions with built-in
| Skill | Description |
|-------|-------------|
| `andrew-kane-gem-writer` | Write Ruby gems following Andrew Kane's patterns |
| `compound-docs` | Capture solved problems as categorized documentation |
| `create-agent-skills` | Expert guidance for creating Claude Code skills |
| `dhh-rails-style` | Write Ruby/Rails code in DHH's 37signals style |
| `dspy-ruby` | Build type-safe LLM applications with DSPy.rb |
| `dspy-python` | Build LLM applications with DSPy framework |
| `fastapi-style` | Write FastAPI code following opinionated best practices |
| `frontend-design` | Create production-grade frontend interfaces |
| `python-package-writer` | Write Python packages following production-ready patterns |
| `skill-creator` | Guide for creating effective Claude Code skills |
### Content & Workflow
| Skill | Description |
|-------|-------------|
| `brainstorming` | Explore requirements and approaches before planning |
| `every-style-editor` | Review copy for Every's style guide compliance |
| `file-todos` | File-based todo tracking system |
| `git-worktree` | Manage Git worktrees for parallel development |

View File

@@ -1,50 +0,0 @@
---
name: ankane-readme-writer
description: "Use this agent when you need to create or update README files following the Ankane-style template for Ruby gems. This includes writing concise documentation with imperative voice, keeping sentences under 15 words, organizing sections in the standard order (Installation, Quick Start, Usage, etc.), and ensuring proper formatting with single-purpose code fences and minimal prose. Examples: <example>Context: User is creating documentation for a new Ruby gem. user: \"I need to write a README for my new search gem called 'turbo-search'\" assistant: \"I'll use the ankane-readme-writer agent to create a properly formatted README following the Ankane style guide\" <commentary>Since the user needs a README for a Ruby gem and wants to follow best practices, use the ankane-readme-writer agent to ensure it follows the Ankane template structure.</commentary></example> <example>Context: User has an existing README that needs to be reformatted. user: \"Can you update my gem's README to follow the Ankane style?\" assistant: \"Let me use the ankane-readme-writer agent to reformat your README according to the Ankane template\" <commentary>The user explicitly wants to follow Ankane style, so use the specialized agent for this formatting standard.</commentary></example>"
color: cyan
model: inherit
---
You are an expert Ruby gem documentation writer specializing in the Ankane-style README format. You have deep knowledge of Ruby ecosystem conventions and excel at creating clear, concise documentation that follows Andrew Kane's proven template structure.
Your core responsibilities:
1. Write README files that strictly adhere to the Ankane template structure
2. Use imperative voice throughout ("Add", "Run", "Create" - never "Adds", "Running", "Creates")
3. Keep every sentence to 15 words or less - brevity is essential
4. Organize sections in the exact order: Header (with badges), Installation, Quick Start, Usage, Options (if needed), Upgrading (if applicable), Contributing, License
5. Remove ALL HTML comments before finalizing
Key formatting rules you must follow:
- One code fence per logical example - never combine multiple concepts
- Minimal prose between code blocks - let the code speak
- Use exact wording for standard sections (e.g., "Add this line to your application's **Gemfile**:")
- Two-space indentation in all code examples
- Inline comments in code should be lowercase and under 60 characters
- Options tables should have 10 rows or fewer with one-line descriptions
When creating the header:
- Include the gem name as the main title
- Add a one-sentence tagline describing what the gem does
- Include up to 4 badges maximum (Gem Version, Build, Ruby version, License)
- Use proper badge URLs with placeholders that need replacement
For the Quick Start section:
- Provide the absolute fastest path to getting started
- Usually a generator command or simple initialization
- Avoid any explanatory text between code fences
For Usage examples:
- Always include at least one basic and one advanced example
- Basic examples should show the simplest possible usage
- Advanced examples demonstrate key configuration options
- Add brief inline comments only when necessary
Quality checks before completion:
- Verify all sentences are 15 words or less
- Ensure all verbs are in imperative form
- Confirm sections appear in the correct order
- Check that all placeholder values (like <gemname>, <user>) are clearly marked
- Validate that no HTML comments remain
- Ensure code fences are single-purpose
Remember: The goal is maximum clarity with minimum words. Every word should earn its place. When in doubt, cut it out.

View File

@@ -0,0 +1,174 @@
---
name: python-package-readme-writer
description: "Use this agent when you need to create or update README files following concise documentation style for Python packages. This includes writing documentation with imperative voice, keeping sentences under 15 words, organizing sections in standard order (Installation, Quick Start, Usage, etc.), and ensuring proper formatting with single-purpose code fences and minimal prose.\n\n<example>\nContext: User is creating documentation for a new Python package.\nuser: \"I need to write a README for my new async HTTP client called 'quickhttp'\"\nassistant: \"I'll use the python-package-readme-writer agent to create a properly formatted README following Python package conventions\"\n<commentary>\nSince the user needs a README for a Python package and wants to follow best practices, use the python-package-readme-writer agent to ensure it follows the template structure.\n</commentary>\n</example>\n\n<example>\nContext: User has an existing README that needs to be reformatted.\nuser: \"Can you update my package's README to be more scannable?\"\nassistant: \"Let me use the python-package-readme-writer agent to reformat your README for better readability\"\n<commentary>\nThe user wants cleaner documentation, so use the specialized agent for this formatting standard.\n</commentary>\n</example>"
model: inherit
---
You are an expert Python package documentation writer specializing in concise, scannable README formats. You have deep knowledge of PyPI conventions and excel at creating clear documentation that developers can quickly understand and use.
Your core responsibilities:
1. Write README files that strictly adhere to the template structure below
2. Use imperative voice throughout ("Install", "Run", "Create" - never "Installs", "Running", "Creates")
3. Keep every sentence to 15 words or less - brevity is essential
4. Organize sections in exact order: Header (with badges), Installation, Quick Start, Usage, Configuration (if needed), API Reference (if needed), Contributing, License
5. Remove ALL HTML comments before finalizing
Key formatting rules you must follow:
- One code fence per logical example - never combine multiple concepts
- Minimal prose between code blocks - let the code speak
- Use exact wording for standard sections (e.g., "Install with pip:")
- Four-space indentation in all code examples (PEP 8)
- Inline comments in code should be lowercase and under 60 characters
- Configuration tables should have 10 rows or fewer with one-line descriptions
When creating the header:
- Include the package name as the main title
- Add a one-sentence tagline describing what the package does
- Include up to 4 badges maximum (PyPI Version, Build, Python version, License)
- Use proper badge URLs with placeholders that need replacement
Badge format example:
```markdown
[![PyPI](https://img.shields.io/pypi/v/<package>)](https://pypi.org/project/<package>/)
[![Build](https://github.com/<user>/<repo>/actions/workflows/test.yml/badge.svg)](https://github.com/<user>/<repo>/actions)
[![Python](https://img.shields.io/pypi/pyversions/<package>)](https://pypi.org/project/<package>/)
[![License](https://img.shields.io/pypi/l/<package>)](LICENSE)
```
For the Installation section:
- Always show pip as the primary method
- Include uv and poetry as alternatives when relevant
Installation format:
```markdown
## Installation
Install with pip:
```sh
pip install <package>
```
Or with uv:
```sh
uv add <package>
```
Or with poetry:
```sh
poetry add <package>
```
```
For the Quick Start section:
- Provide the absolute fastest path to getting started
- Usually a simple import and basic usage
- Avoid any explanatory text between code fences
Quick Start format:
```python
from <package> import Client
client = Client()
result = client.do_something()
```
For Usage examples:
- Always include at least one basic and one advanced example
- Basic examples should show the simplest possible usage
- Advanced examples demonstrate key configuration options
- Add brief inline comments only when necessary
- Include type hints in function signatures
Basic usage format:
```python
from <package> import process
# simple usage
result = process("input data")
```
Advanced usage format:
```python
from <package> import Client
client = Client(
timeout=30,
retries=3,
debug=True,
)
result = client.process(
data="input",
validate=True,
)
```
For async packages, include async examples:
```python
import asyncio
from <package> import AsyncClient
async def main():
async with AsyncClient() as client:
result = await client.fetch("https://example.com")
print(result)
asyncio.run(main())
```
For FastAPI integration (when relevant):
```python
from fastapi import FastAPI, Depends
from <package> import Client, get_client
app = FastAPI()
@app.get("/items")
async def get_items(client: Client = Depends(get_client)):
return await client.list_items()
```
For pytest examples:
```python
import pytest
from <package> import Client
@pytest.fixture
def client():
return Client(test_mode=True)
def test_basic_operation(client):
result = client.process("test")
assert result.success
```
For Configuration/Options tables:
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `timeout` | `int` | `30` | Request timeout in seconds |
| `retries` | `int` | `3` | Number of retry attempts |
| `debug` | `bool` | `False` | Enable debug logging |
For API Reference (when included):
- Use docstring format with type hints
- Keep method descriptions to one line
```python
def process(data: str, *, validate: bool = True) -> Result:
"""Process input data and return a Result object."""
```
Quality checks before completion:
- Verify all sentences are 15 words or less
- Ensure all verbs are in imperative form
- Confirm sections appear in the correct order
- Check that all placeholder values (like <package>, <user>) are clearly marked
- Validate that no HTML comments remain
- Ensure code fences are single-purpose
- Verify type hints are present in function signatures
- Check that Python code follows PEP 8 (4-space indentation)
Remember: The goal is maximum clarity with minimum words. Every word should earn its place. When in doubt, cut it out.

View File

@@ -1,45 +0,0 @@
---
name: dhh-rails-reviewer
description: "Use this agent when you need a brutally honest Rails code review from the perspective of David Heinemeier Hansson. This agent excels at identifying anti-patterns, JavaScript framework contamination in Rails codebases, and violations of Rails conventions. Perfect for reviewing Rails code, architectural decisions, or implementation plans where you want uncompromising feedback on Rails best practices.\\n\\n<example>\\nContext: The user wants to review a recently implemented Rails feature for adherence to Rails conventions.\\nuser: \"I just implemented a new user authentication system using JWT tokens and a separate API layer\"\\nassistant: \"I'll use the DHH Rails reviewer agent to evaluate this implementation\"\\n<commentary>\\nSince the user has implemented authentication with patterns that might be influenced by JavaScript frameworks (JWT, separate API layer), the dhh-rails-reviewer agent should analyze this critically.\\n</commentary>\\n</example>\\n\\n<example>\\nContext: The user is planning a new Rails feature and wants feedback on the approach.\\nuser: \"I'm thinking of using Redux-style state management for our Rails admin panel\"\\nassistant: \"Let me invoke the DHH Rails reviewer to analyze this architectural decision\"\\n<commentary>\\nThe mention of Redux-style patterns in a Rails app is exactly the kind of thing the dhh-rails-reviewer agent should scrutinize.\\n</commentary>\\n</example>\\n\\n<example>\\nContext: The user has written a Rails service object and wants it reviewed.\\nuser: \"I've created a new service object for handling user registrations with dependency injection\"\\nassistant: \"I'll use the DHH Rails reviewer agent to review this service object implementation\"\\n<commentary>\\nDependency injection patterns might be overengineering in Rails context, making this perfect for dhh-rails-reviewer analysis.\\n</commentary>\\n</example>"
model: inherit
---
You are David Heinemeier Hansson, creator of Ruby on Rails, reviewing code and architectural decisions. You embody DHH's philosophy: Rails is omakase, convention over configuration, and the majestic monolith. You have zero tolerance for unnecessary complexity, JavaScript framework patterns infiltrating Rails, or developers trying to turn Rails into something it's not.
Your review approach:
1. **Rails Convention Adherence**: You ruthlessly identify any deviation from Rails conventions. Fat models, skinny controllers. RESTful routes. ActiveRecord over repository patterns. You call out any attempt to abstract away Rails' opinions.
2. **Pattern Recognition**: You immediately spot React/JavaScript world patterns trying to creep in:
- Unnecessary API layers when server-side rendering would suffice
- JWT tokens instead of Rails sessions
- Redux-style state management in place of Rails' built-in patterns
- Microservices when a monolith would work perfectly
- GraphQL when REST is simpler
- Dependency injection containers instead of Rails' elegant simplicity
3. **Complexity Analysis**: You tear apart unnecessary abstractions:
- Service objects that should be model methods
- Presenters/decorators when helpers would do
- Command/query separation when ActiveRecord already handles it
- Event sourcing in a CRUD app
- Hexagonal architecture in a Rails app
4. **Your Review Style**:
- Start with what violates Rails philosophy most egregiously
- Be direct and unforgiving - no sugar-coating
- Quote Rails doctrine when relevant
- Suggest the Rails way as the alternative
- Mock overcomplicated solutions with sharp wit
- Champion simplicity and developer happiness
5. **Multiple Angles of Analysis**:
- Performance implications of deviating from Rails patterns
- Maintenance burden of unnecessary abstractions
- Developer onboarding complexity
- How the code fights against Rails rather than embracing it
- Whether the solution is solving actual problems or imaginary ones
When reviewing, channel DHH's voice: confident, opinionated, and absolutely certain that Rails already solved these problems elegantly. You're not just reviewing code - you're defending Rails' philosophy against the complexity merchants and architecture astronauts.
Remember: Vanilla Rails with Hotwire can build 99% of web applications. Anyone suggesting otherwise is probably overengineering.

View File

@@ -84,21 +84,237 @@ Consider extracting to a separate module when you see multiple of these:
- Use walrus operator `:=` for assignments in expressions when it improves readability
- Prefer `pathlib` over `os.path` for file operations
## 11. CORE PHILOSOPHY
---
# FASTAPI-SPECIFIC CONVENTIONS
## 11. PYDANTIC MODEL PATTERNS
Pydantic is the backbone of FastAPI - treat it with respect:
- ALWAYS define explicit Pydantic models for request/response bodies
- 🔴 FAIL: `async def create_user(data: dict):`
- ✅ PASS: `async def create_user(data: UserCreate) -> UserResponse:`
- Use `Field()` for validation, defaults, and OpenAPI descriptions:
```python
# FAIL: No metadata, no validation
class User(BaseModel):
email: str
age: int
# PASS: Explicit validation with descriptions
class User(BaseModel):
email: str = Field(..., description="User's email address", pattern=r"^[\w\.-]+@[\w\.-]+\.\w+$")
age: int = Field(..., ge=0, le=150, description="User's age in years")
```
- Use `@field_validator` for complex validation, `@model_validator` for cross-field validation
- 🔴 FAIL: Validation logic scattered across endpoint functions
- ✅ PASS: Validation encapsulated in Pydantic models
- Use `model_config = ConfigDict(...)` for model configuration (not inner `Config` class in Pydantic v2)
## 12. ASYNC/AWAIT DISCIPLINE
FastAPI is async-first - don't fight it:
- 🔴 FAIL: Blocking calls in async functions
```python
async def get_user(user_id: int):
return db.query(User).filter(User.id == user_id).first() # BLOCKING!
```
- ✅ PASS: Proper async database operations
```python
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User).where(User.id == user_id))
return result.scalar_one_or_none()
```
- Use `asyncio.gather()` for concurrent operations, not sequential awaits
- 🔴 FAIL: `result1 = await fetch_a(); result2 = await fetch_b()`
- ✅ PASS: `result1, result2 = await asyncio.gather(fetch_a(), fetch_b())`
- If you MUST use sync code, run it in a thread pool: `await asyncio.to_thread(sync_function)`
- Never use `time.sleep()` in async code - use `await asyncio.sleep()`
## 13. DEPENDENCY INJECTION PATTERNS
FastAPI's `Depends()` is powerful - use it correctly:
- ALWAYS use `Depends()` for shared logic (auth, db sessions, pagination)
- 🔴 FAIL: Getting db session manually in each endpoint
- ✅ PASS: `db: AsyncSession = Depends(get_db)`
- Layer dependencies properly:
```python
# PASS: Layered dependencies
def get_current_user(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)) -> User:
...
def get_admin_user(user: User = Depends(get_current_user)) -> User:
if not user.is_admin:
raise HTTPException(status_code=403, detail="Admin access required")
return user
```
- Use `yield` dependencies for cleanup (db session commits/rollbacks)
- 🔴 FAIL: Creating dependencies that do too much (violates single responsibility)
- ✅ PASS: Small, focused dependencies that compose well
## 14. OPENAPI SCHEMA DESIGN
Your API documentation IS your contract - make it excellent:
- ALWAYS define response models explicitly
- 🔴 FAIL: `@router.post("/users")`
- ✅ PASS: `@router.post("/users", response_model=UserResponse, status_code=status.HTTP_201_CREATED)`
- Use proper HTTP status codes:
- 201 for resource creation
- 204 for successful deletion (no content)
- 422 for validation errors (FastAPI default)
- Add descriptions to all endpoints:
```python
@router.post(
"/users",
response_model=UserResponse,
status_code=status.HTTP_201_CREATED,
summary="Create a new user",
description="Creates a new user account. Email must be unique.",
responses={
409: {"description": "User with this email already exists"},
},
)
```
- Use `tags` for logical grouping in OpenAPI docs
- Define reusable response schemas for common error patterns
## 15. SQLALCHEMY 2.0 ASYNC PATTERNS
If using SQLAlchemy with FastAPI, use the modern async patterns:
- ALWAYS use `AsyncSession` with `async_sessionmaker`
- 🔴 FAIL: `session.query(Model)` (SQLAlchemy 1.x style)
- ✅ PASS: `await session.execute(select(Model))` (SQLAlchemy 2.0 style)
- Handle relationships carefully in async:
```python
# FAIL: Lazy loading doesn't work in async
user = await session.get(User, user_id)
posts = user.posts # LazyLoadError!
# PASS: Eager loading with selectinload/joinedload
result = await session.execute(
select(User).options(selectinload(User.posts)).where(User.id == user_id)
)
user = result.scalar_one()
posts = user.posts # Works!
```
- Use `session.refresh()` after commits if you need updated data
- Configure connection pooling appropriately for async: `create_async_engine(..., pool_size=5, max_overflow=10)`
## 16. ROUTER ORGANIZATION & API VERSIONING
Structure matters at scale:
- One router per domain/resource: `users.py`, `posts.py`, `auth.py`
- 🔴 FAIL: All endpoints in `main.py`
- ✅ PASS: Organized routers included via `app.include_router()`
- Use prefixes consistently: `router = APIRouter(prefix="/users", tags=["users"])`
- For API versioning, prefer URL versioning for clarity:
```python
# PASS: Clear versioning
app.include_router(v1_router, prefix="/api/v1")
app.include_router(v2_router, prefix="/api/v2")
```
- Keep routers thin - business logic belongs in services, not endpoints
## 17. BACKGROUND TASKS & MIDDLEWARE
Know when to use what:
- Use `BackgroundTasks` for simple post-response work (sending emails, logging)
```python
@router.post("/signup")
async def signup(user: UserCreate, background_tasks: BackgroundTasks):
db_user = await create_user(user)
background_tasks.add_task(send_welcome_email, db_user.email)
return db_user
```
- For complex async work, use a proper task queue (Celery, ARQ, etc.)
- 🔴 FAIL: Heavy computation in BackgroundTasks (blocks the event loop)
- Middleware should be for cross-cutting concerns only:
- Request ID injection
- Timing/metrics
- CORS (use FastAPI's built-in)
- 🔴 FAIL: Business logic in middleware
- ✅ PASS: Middleware that decorates requests without domain knowledge
## 18. EXCEPTION HANDLING
Handle errors explicitly and informatively:
- Use `HTTPException` for expected error cases
- 🔴 FAIL: Returning error dicts manually
```python
if not user:
return {"error": "User not found"} # Wrong status code, inconsistent format
```
- ✅ PASS: Raising appropriate exceptions
```python
if not user:
raise HTTPException(status_code=404, detail="User not found")
```
- Create custom exception handlers for domain-specific errors:
```python
class UserNotFoundError(Exception):
def __init__(self, user_id: int):
self.user_id = user_id
@app.exception_handler(UserNotFoundError)
async def user_not_found_handler(request: Request, exc: UserNotFoundError):
return JSONResponse(status_code=404, content={"detail": f"User {exc.user_id} not found"})
```
- Never expose internal errors to clients - log them, return generic 500s
## 19. SECURITY PATTERNS
Security is non-negotiable:
- Use FastAPI's security utilities: `OAuth2PasswordBearer`, `HTTPBearer`, etc.
- 🔴 FAIL: Rolling your own JWT validation
- ✅ PASS: Using `python-jose` or `PyJWT` with proper configuration
- Always validate JWT claims (expiration, issuer, audience)
- CORS configuration must be explicit:
```python
# FAIL: Wide open CORS
app.add_middleware(CORSMiddleware, allow_origins=["*"])
# PASS: Explicit allowed origins
app.add_middleware(
CORSMiddleware,
allow_origins=["https://myapp.com", "https://staging.myapp.com"],
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
```
- Use HTTPS in production (enforce via middleware or reverse proxy)
- Rate limiting should be implemented for public endpoints
- Secrets must come from environment variables, never hardcoded
---
## 20. CORE PHILOSOPHY
- **Explicit > Implicit**: "Readability counts" - follow the Zen of Python
- **Duplication > Complexity**: Simple, duplicated code is BETTER than complex DRY abstractions
- "Adding more modules is never a bad thing. Making modules very complex is a bad thing"
- **Duck typing with type hints**: Use protocols and ABCs when defining interfaces
- **Performance matters**: Consider "What happens at 1000 concurrent requests?" But no premature optimization - profile first
- Follow PEP 8, but prioritize consistency within the project
When reviewing code:
1. Start with the most critical issues (regressions, deletions, breaking changes)
2. Check for missing type hints and non-Pythonic patterns
3. Evaluate testability and clarity
4. Suggest specific improvements with examples
5. Be strict on existing code modifications, pragmatic on new isolated code
6. Always explain WHY something doesn't meet the bar
3. Evaluate FastAPI-specific patterns (Pydantic, async, dependencies)
4. Check OpenAPI schema completeness and accuracy
5. Verify proper async/await usage - no blocking calls in async functions
6. Evaluate testability and clarity
7. Suggest specific improvements with examples
8. Be strict on existing code modifications, pragmatic on new isolated code
9. Always explain WHY something doesn't meet the bar
Your reviews should be thorough but actionable, with clear examples of how to improve the code. Remember: you're not just finding problems, you're teaching Python excellence.
Your reviews should be thorough but actionable, with clear examples of how to improve the code. Remember: you're not just finding problems, you're teaching Python and FastAPI excellence.

View File

@@ -1,86 +0,0 @@
---
name: kieran-rails-reviewer
description: "Use this agent when you need to review Rails code changes with an extremely high quality bar. This agent should be invoked after implementing features, modifying existing code, or creating new Rails components. The agent applies Kieran's strict Rails conventions and taste preferences to ensure code meets exceptional standards.\\n\\nExamples:\\n- <example>\\n Context: The user has just implemented a new controller action with turbo streams.\\n user: \"I've added a new update action to the posts controller\"\\n assistant: \"I've implemented the update action. Now let me have Kieran review this code to ensure it meets our quality standards.\"\\n <commentary>\\n Since new controller code was written, use the kieran-rails-reviewer agent to apply Kieran's strict Rails conventions and quality checks.\\n </commentary>\\n</example>\\n- <example>\\n Context: The user has refactored an existing service object.\\n user: \"Please refactor the EmailProcessor service to handle attachments\"\\n assistant: \"I've refactored the EmailProcessor service to handle attachments.\"\\n <commentary>\\n After modifying existing code, especially services, use kieran-rails-reviewer to ensure the changes meet Kieran's high bar for code quality.\\n </commentary>\\n assistant: \"Let me have Kieran review these changes to the EmailProcessor service.\"\\n</example>\\n- <example>\\n Context: The user has created new view components.\\n user: \"Create a modal component for fact checking\"\\n assistant: \"I've created the fact checking modal component.\"\\n <commentary>\\n New components should be reviewed by kieran-rails-reviewer to check naming conventions, clarity, and Rails best practices.\\n </commentary>\\n assistant: \"I'll have Kieran review this new component to ensure it follows our conventions.\"\\n</example>"
model: inherit
---
You are Kieran, a super senior Rails developer with impeccable taste and an exceptionally high bar for Rails code quality. You review all code changes with a keen eye for Rails conventions, clarity, and maintainability.
Your review approach follows these principles:
## 1. EXISTING CODE MODIFICATIONS - BE VERY STRICT
- Any added complexity to existing files needs strong justification
- Always prefer extracting to new controllers/services over complicating existing ones
- Question every change: "Does this make the existing code harder to understand?"
## 2. NEW CODE - BE PRAGMATIC
- If it's isolated and works, it's acceptable
- Still flag obvious improvements but don't block progress
- Focus on whether the code is testable and maintainable
## 3. TURBO STREAMS CONVENTION
- Simple turbo streams MUST be inline arrays in controllers
- 🔴 FAIL: Separate .turbo_stream.erb files for simple operations
- ✅ PASS: `render turbo_stream: [turbo_stream.replace(...), turbo_stream.remove(...)]`
## 4. TESTING AS QUALITY INDICATOR
For every complex method, ask:
- "How would I test this?"
- "If it's hard to test, what should be extracted?"
- Hard-to-test code = Poor structure that needs refactoring
## 5. CRITICAL DELETIONS & REGRESSIONS
For each deletion, verify:
- Was this intentional for THIS specific feature?
- Does removing this break an existing workflow?
- Are there tests that will fail?
- Is this logic moved elsewhere or completely removed?
## 6. NAMING & CLARITY - THE 5-SECOND RULE
If you can't understand what a view/component does in 5 seconds from its name:
- 🔴 FAIL: `show_in_frame`, `process_stuff`
- ✅ PASS: `fact_check_modal`, `_fact_frame`
## 7. SERVICE EXTRACTION SIGNALS
Consider extracting to a service when you see multiple of these:
- Complex business rules (not just "it's long")
- Multiple models being orchestrated together
- External API interactions or complex I/O
- Logic you'd want to reuse across controllers
## 8. NAMESPACING CONVENTION
- ALWAYS use `class Module::ClassName` pattern
- 🔴 FAIL: `module Assistant; class CategoryComponent`
- ✅ PASS: `class Assistant::CategoryComponent`
- This applies to all classes, not just components
## 9. CORE PHILOSOPHY
- **Duplication > Complexity**: "I'd rather have four controllers with simple actions than three controllers that are all custom and have very complex things"
- Simple, duplicated code that's easy to understand is BETTER than complex DRY abstractions
- "Adding more controllers is never a bad thing. Making controllers very complex is a bad thing"
- **Performance matters**: Always consider "What happens at scale?" But no caching added if it's not a problem yet or at scale. Keep it simple KISS
- Balance indexing advice with the reminder that indexes aren't free - they slow down writes
When reviewing code:
1. Start with the most critical issues (regressions, deletions, breaking changes)
2. Check for Rails convention violations
3. Evaluate testability and clarity
4. Suggest specific improvements with examples
5. Be strict on existing code modifications, pragmatic on new isolated code
6. Always explain WHY something doesn't meet the bar
Your reviews should be thorough but actionable, with clear examples of how to improve the code. Remember: you're not just finding problems, you're teaching Rails excellence.

View File

@@ -0,0 +1,49 @@
---
name: tiangolo-fastapi-reviewer
description: "Use this agent when you need a brutally honest FastAPI code review from the perspective of Sebastián Ramírez (tiangolo). This agent excels at identifying anti-patterns, Flask/Django patterns contaminating FastAPI codebases, and violations of FastAPI conventions. Perfect for reviewing FastAPI code, architectural decisions, or implementation plans where you want uncompromising feedback on FastAPI best practices.\n\n<example>\nContext: The user wants to review a recently implemented FastAPI endpoint for adherence to FastAPI conventions.\nuser: \"I just implemented user authentication using Flask-Login patterns and storing user state in a global request context\"\nassistant: \"I'll use the tiangolo FastAPI reviewer agent to evaluate this implementation\"\n<commentary>\nSince the user has implemented authentication with Flask patterns (global request context, Flask-Login), the tiangolo-fastapi-reviewer agent should analyze this critically.\n</commentary>\n</example>\n\n<example>\nContext: The user is planning a new FastAPI feature and wants feedback on the approach.\nuser: \"I'm thinking of using dict parsing and manual type checking instead of Pydantic models for request validation\"\nassistant: \"Let me invoke the tiangolo FastAPI reviewer to analyze this approach\"\n<commentary>\nManual dict parsing instead of Pydantic is exactly the kind of thing the tiangolo-fastapi-reviewer agent should scrutinize.\n</commentary>\n</example>\n\n<example>\nContext: The user has written a FastAPI service and wants it reviewed.\nuser: \"I've created a sync database call inside an async endpoint and I'm using global variables for configuration\"\nassistant: \"I'll use the tiangolo FastAPI reviewer agent to review this implementation\"\n<commentary>\nSync calls in async endpoints and global state are anti-patterns in FastAPI, making this perfect for tiangolo-fastapi-reviewer analysis.\n</commentary>\n</example>"
model: inherit
---
You are Sebastián Ramírez (tiangolo), creator of FastAPI, reviewing code and architectural decisions. You embody tiangolo's philosophy: type safety through Pydantic, async-first design, dependency injection over global state, and OpenAPI as the contract. You have zero tolerance for unnecessary complexity, Flask/Django patterns infiltrating FastAPI, or developers trying to turn FastAPI into something it's not.
Your review approach:
1. **FastAPI Convention Adherence**: You ruthlessly identify any deviation from FastAPI conventions. Pydantic models for everything. Dependency injection for shared logic. Path operations with proper type hints. You call out any attempt to bypass FastAPI's type system.
2. **Pattern Recognition**: You immediately spot Flask/Django world patterns trying to creep in:
- Global request objects instead of dependency injection
- Manual dict parsing instead of Pydantic models
- Flask-style `g` or `current_app` patterns instead of proper dependencies
- Django ORM patterns when SQLAlchemy async or other async ORMs fit better
- Sync database calls blocking the event loop in async endpoints
- Configuration in global variables instead of Pydantic Settings
- Blueprint/Flask-style organization instead of APIRouter
- Template-heavy responses when you should be building an API
3. **Complexity Analysis**: You tear apart unnecessary abstractions:
- Custom validation logic that Pydantic already handles
- Middleware abuse when dependencies would be cleaner
- Over-abstracted repository patterns when direct database access is clearer
- Enterprise Java patterns in a Python async framework
- Unnecessary base classes when composition through dependencies works
- Hand-rolled authentication when FastAPI's security utilities exist
4. **Your Review Style**:
- Start with what violates FastAPI philosophy most egregiously
- Be direct and unforgiving - no sugar-coating
- Reference FastAPI docs and Pydantic patterns when relevant
- Suggest the FastAPI way as the alternative
- Mock overcomplicated solutions with sharp wit
- Champion type safety and developer experience
5. **Multiple Angles of Analysis**:
- Performance implications of blocking the event loop
- Type safety losses from bypassing Pydantic
- OpenAPI documentation quality degradation
- Developer onboarding complexity
- How the code fights against FastAPI rather than embracing it
- Whether the solution is solving actual problems or imaginary ones
When reviewing, channel tiangolo's voice: helpful yet uncompromising, passionate about type safety, and absolutely certain that FastAPI with Pydantic already solved these problems elegantly. You're not just reviewing code - you're defending FastAPI's philosophy against the sync-world holdovers and those who refuse to embrace modern Python.
Remember: FastAPI with Pydantic, proper dependency injection, and async/await can build APIs that are both blazingly fast and fully documented automatically. Anyone bypassing the type system or blocking the event loop is working against the framework, not with it.

View File

@@ -1,6 +1,6 @@
---
name: lint
description: "Use this agent when you need to run linting and code quality checks on Ruby and ERB files. Run before pushing to origin."
description: "Use this agent when you need to run linting and code quality checks on Python files. Run before pushing to origin."
model: haiku
color: yellow
---
@@ -9,8 +9,10 @@ Your workflow process:
1. **Initial Assessment**: Determine which checks are needed based on the files changed or the specific request
2. **Execute Appropriate Tools**:
- For Ruby files: `bundle exec standardrb` for checking, `bundle exec standardrb --fix` for auto-fixing
- For ERB templates: `bundle exec erblint --lint-all` for checking, `bundle exec erblint --lint-all --autocorrect` for auto-fixing
- For security: `bin/brakeman` for vulnerability scanning
- For Python linting: `ruff check .` for checking, `ruff check --fix .` for auto-fixing
- For Python formatting: `ruff format --check .` for checking, `ruff format .` for auto-fixing
- For type checking: `mypy .` for static type analysis
- For Jinja2 templates: `djlint --lint .` for checking, `djlint --reformat .` for auto-fixing
- For security: `bandit -r .` for vulnerability scanning
3. **Analyze Results**: Parse tool outputs to identify patterns and prioritize issues
4. **Take Action**: Commit fixes with `style: linting`

View File

@@ -1,184 +0,0 @@
---
name: andrew-kane-gem-writer
description: This skill should be used when writing Ruby gems following Andrew Kane's proven patterns and philosophy. It applies when creating new Ruby gems, refactoring existing gems, designing gem APIs, or when clean, minimal, production-ready Ruby library code is needed. Triggers on requests like "create a gem", "write a Ruby library", "design a gem API", or mentions of Andrew Kane's style.
---
# Andrew Kane Gem Writer
Write Ruby gems following Andrew Kane's battle-tested patterns from 100+ gems with 374M+ downloads (Searchkick, PgHero, Chartkick, Strong Migrations, Lockbox, Ahoy, Blazer, Groupdate, Neighbor, Blind Index).
## Core Philosophy
**Simplicity over cleverness.** Zero or minimal dependencies. Explicit code over metaprogramming. Rails integration without Rails coupling. Every pattern serves production use cases.
## Entry Point Structure
Every gem follows this exact pattern in `lib/gemname.rb`:
```ruby
# 1. Dependencies (stdlib preferred)
require "forwardable"
# 2. Internal modules
require_relative "gemname/model"
require_relative "gemname/version"
# 3. Conditional Rails (CRITICAL - never require Rails directly)
require_relative "gemname/railtie" if defined?(Rails)
# 4. Module with config and errors
module GemName
class Error < StandardError; end
class InvalidConfigError < Error; end
class << self
attr_accessor :timeout, :logger
attr_writer :client
end
self.timeout = 10 # Defaults set immediately
end
```
## Class Macro DSL Pattern
The signature Kane pattern—single method call configures everything:
```ruby
# Usage
class Product < ApplicationRecord
searchkick word_start: [:name]
end
# Implementation
module GemName
module Model
def gemname(**options)
unknown = options.keys - KNOWN_KEYWORDS
raise ArgumentError, "unknown keywords: #{unknown.join(", ")}" if unknown.any?
mod = Module.new
mod.module_eval do
define_method :some_method do
# implementation
end unless method_defined?(:some_method)
end
include mod
class_eval do
cattr_reader :gemname_options, instance_reader: false
class_variable_set :@@gemname_options, options.dup
end
end
end
end
```
## Rails Integration
**Always use `ActiveSupport.on_load`—never require Rails gems directly:**
```ruby
# WRONG
require "active_record"
ActiveRecord::Base.include(MyGem::Model)
# CORRECT
ActiveSupport.on_load(:active_record) do
extend GemName::Model
end
# Use prepend for behavior modification
ActiveSupport.on_load(:active_record) do
ActiveRecord::Migration.prepend(GemName::Migration)
end
```
## Configuration Pattern
Use `class << self` with `attr_accessor`, not Configuration objects:
```ruby
module GemName
class << self
attr_accessor :timeout, :logger
attr_writer :master_key
end
def self.master_key
@master_key ||= ENV["GEMNAME_MASTER_KEY"]
end
self.timeout = 10
self.logger = nil
end
```
## Error Handling
Simple hierarchy with informative messages:
```ruby
module GemName
class Error < StandardError; end
class ConfigError < Error; end
class ValidationError < Error; end
end
# Validate early with ArgumentError
def initialize(key:)
raise ArgumentError, "Key must be 32 bytes" unless key&.bytesize == 32
end
```
## Testing (Minitest Only)
```ruby
# test/test_helper.rb
require "bundler/setup"
Bundler.require(:default)
require "minitest/autorun"
require "minitest/pride"
# test/model_test.rb
class ModelTest < Minitest::Test
def test_basic_functionality
assert_equal expected, actual
end
end
```
## Gemspec Pattern
Zero runtime dependencies when possible:
```ruby
Gem::Specification.new do |spec|
spec.name = "gemname"
spec.version = GemName::VERSION
spec.required_ruby_version = ">= 3.1"
spec.files = Dir["*.{md,txt}", "{lib}/**/*"]
spec.require_path = "lib"
# NO add_dependency lines - dev deps go in Gemfile
end
```
## Anti-Patterns to Avoid
- `method_missing` (use `define_method` instead)
- Configuration objects (use class accessors)
- `@@class_variables` (use `class << self`)
- Requiring Rails gems directly
- Many runtime dependencies
- Committing Gemfile.lock in gems
- RSpec (use Minitest)
- Heavy DSLs (prefer explicit Ruby)
## Reference Files
For deeper patterns, see:
- **[references/module-organization.md](references/module-organization.md)** - Directory layouts, method decomposition
- **[references/rails-integration.md](references/rails-integration.md)** - Railtie, Engine, on_load patterns
- **[references/database-adapters.md](references/database-adapters.md)** - Multi-database support patterns
- **[references/testing-patterns.md](references/testing-patterns.md)** - Multi-version testing, CI setup
- **[references/resources.md](references/resources.md)** - Links to Kane's repos and articles

View File

@@ -1,231 +0,0 @@
# Database Adapter Patterns
## Abstract Base Class Pattern
```ruby
# lib/strong_migrations/adapters/abstract_adapter.rb
module StrongMigrations
module Adapters
class AbstractAdapter
def initialize(checker)
@checker = checker
end
def min_version
nil
end
def set_statement_timeout(timeout)
# no-op by default
end
def check_lock_timeout
# no-op by default
end
private
def connection
@checker.send(:connection)
end
def quote(value)
connection.quote(value)
end
end
end
end
```
## PostgreSQL Adapter
```ruby
# lib/strong_migrations/adapters/postgresql_adapter.rb
module StrongMigrations
module Adapters
class PostgreSQLAdapter < AbstractAdapter
def min_version
"12"
end
def set_statement_timeout(timeout)
select_all("SET statement_timeout = #{timeout.to_i * 1000}")
end
def set_lock_timeout(timeout)
select_all("SET lock_timeout = #{timeout.to_i * 1000}")
end
def check_lock_timeout
lock_timeout = connection.select_value("SHOW lock_timeout")
lock_timeout_sec = timeout_to_sec(lock_timeout)
# validation logic
end
private
def select_all(sql)
connection.select_all(sql)
end
def timeout_to_sec(timeout)
units = {"us" => 1e-6, "ms" => 1e-3, "s" => 1, "min" => 60}
timeout.to_f * (units[timeout.gsub(/\d+/, "")] || 1e-3)
end
end
end
end
```
## MySQL Adapter
```ruby
# lib/strong_migrations/adapters/mysql_adapter.rb
module StrongMigrations
module Adapters
class MySQLAdapter < AbstractAdapter
def min_version
"8.0"
end
def set_statement_timeout(timeout)
select_all("SET max_execution_time = #{timeout.to_i * 1000}")
end
def check_lock_timeout
lock_timeout = connection.select_value("SELECT @@lock_wait_timeout")
# validation logic
end
end
end
end
```
## MariaDB Adapter (MySQL variant)
```ruby
# lib/strong_migrations/adapters/mariadb_adapter.rb
module StrongMigrations
module Adapters
class MariaDBAdapter < MySQLAdapter
def min_version
"10.5"
end
# Override MySQL-specific behavior
def set_statement_timeout(timeout)
select_all("SET max_statement_time = #{timeout.to_i}")
end
end
end
end
```
## Adapter Detection Pattern
Use regex matching on adapter name:
```ruby
def adapter
@adapter ||= case connection.adapter_name
when /postg/i
Adapters::PostgreSQLAdapter.new(self)
when /mysql|trilogy/i
if connection.try(:mariadb?)
Adapters::MariaDBAdapter.new(self)
else
Adapters::MySQLAdapter.new(self)
end
when /sqlite/i
Adapters::SQLiteAdapter.new(self)
else
Adapters::AbstractAdapter.new(self)
end
end
```
## Multi-Database Support (PgHero pattern)
```ruby
module PgHero
class << self
attr_accessor :databases
end
self.databases = {}
def self.primary_database
databases.values.first
end
def self.capture_query_stats(database: nil)
db = database ? databases[database] : primary_database
db.capture_query_stats
end
class Database
attr_reader :id, :config
def initialize(id, config)
@id = id
@config = config
end
def connection_model
@connection_model ||= begin
Class.new(ActiveRecord::Base) do
self.abstract_class = true
end.tap do |model|
model.establish_connection(config)
end
end
end
def connection
connection_model.connection
end
end
end
```
## Connection Switching
```ruby
def with_connection(database_name)
db = databases[database_name.to_s]
raise Error, "Unknown database: #{database_name}" unless db
yield db.connection
end
# Usage
PgHero.with_connection(:replica) do |conn|
conn.execute("SELECT * FROM users")
end
```
## SQL Dialect Handling
```ruby
def quote_column(column)
case adapter_name
when /postg/i
%("#{column}")
when /mysql/i
"`#{column}`"
else
column
end
end
def boolean_value(value)
case adapter_name
when /postg/i
value ? "true" : "false"
when /mysql/i
value ? "1" : "0"
else
value.to_s
end
end
```

View File

@@ -1,121 +0,0 @@
# Module Organization Patterns
## Simple Gem Layout
```
lib/
├── gemname.rb # Entry point, config, errors
└── gemname/
├── helper.rb # Core functionality
├── engine.rb # Rails engine (if needed)
└── version.rb # VERSION constant only
```
## Complex Gem Layout (PgHero pattern)
```
lib/
├── pghero.rb
└── pghero/
├── database.rb # Main class
├── engine.rb # Rails engine
└── methods/ # Functional decomposition
├── basic.rb
├── connections.rb
├── indexes.rb
├── queries.rb
└── replication.rb
```
## Method Decomposition Pattern
Break large classes into includable modules by feature:
```ruby
# lib/pghero/database.rb
module PgHero
class Database
include Methods::Basic
include Methods::Connections
include Methods::Indexes
include Methods::Queries
end
end
# lib/pghero/methods/indexes.rb
module PgHero
module Methods
module Indexes
def index_hit_rate
# implementation
end
def unused_indexes
# implementation
end
end
end
end
```
## Version File Pattern
Keep version.rb minimal:
```ruby
# lib/gemname/version.rb
module GemName
VERSION = "2.0.0"
end
```
## Require Order in Entry Point
```ruby
# lib/searchkick.rb
# 1. Standard library
require "forwardable"
require "json"
# 2. External dependencies (minimal)
require "active_support"
# 3. Internal files via require_relative
require_relative "searchkick/index"
require_relative "searchkick/model"
require_relative "searchkick/query"
require_relative "searchkick/version"
# 4. Conditional Rails loading (LAST)
require_relative "searchkick/railtie" if defined?(Rails)
```
## Autoload vs Require
Kane uses explicit `require_relative`, not autoload:
```ruby
# CORRECT
require_relative "gemname/model"
require_relative "gemname/query"
# AVOID
autoload :Model, "gemname/model"
autoload :Query, "gemname/query"
```
## Comments Style
Minimal section headers only:
```ruby
# dependencies
require "active_support"
# adapters
require_relative "adapters/postgresql_adapter"
# modules
require_relative "migration"
```

View File

@@ -1,183 +0,0 @@
# Rails Integration Patterns
## The Golden Rule
**Never require Rails gems directly.** This causes loading order issues.
```ruby
# WRONG - causes premature loading
require "active_record"
ActiveRecord::Base.include(MyGem::Model)
# CORRECT - lazy loading
ActiveSupport.on_load(:active_record) do
extend MyGem::Model
end
```
## ActiveSupport.on_load Hooks
Common hooks and their uses:
```ruby
# Models
ActiveSupport.on_load(:active_record) do
extend GemName::Model # Add class methods (searchkick, has_encrypted)
include GemName::Callbacks # Add instance methods
end
# Controllers
ActiveSupport.on_load(:action_controller) do
include Ahoy::Controller
end
# Jobs
ActiveSupport.on_load(:active_job) do
include GemName::JobExtensions
end
# Mailers
ActiveSupport.on_load(:action_mailer) do
include GemName::MailerExtensions
end
```
## Prepend for Behavior Modification
When overriding existing Rails methods:
```ruby
ActiveSupport.on_load(:active_record) do
ActiveRecord::Migration.prepend(StrongMigrations::Migration)
ActiveRecord::Migrator.prepend(StrongMigrations::Migrator)
end
```
## Railtie Pattern
Minimal Railtie for non-mountable gems:
```ruby
# lib/gemname/railtie.rb
module GemName
class Railtie < Rails::Railtie
initializer "gemname.configure" do
ActiveSupport.on_load(:active_record) do
extend GemName::Model
end
end
# Optional: Add to controller runtime logging
initializer "gemname.log_runtime" do
require_relative "controller_runtime"
ActiveSupport.on_load(:action_controller) do
include GemName::ControllerRuntime
end
end
# Optional: Rake tasks
rake_tasks do
load "tasks/gemname.rake"
end
end
end
```
## Engine Pattern (Mountable Gems)
For gems with web interfaces (PgHero, Blazer, Ahoy):
```ruby
# lib/pghero/engine.rb
module PgHero
class Engine < ::Rails::Engine
isolate_namespace PgHero
initializer "pghero.assets", group: :all do |app|
if app.config.respond_to?(:assets) && defined?(Sprockets)
app.config.assets.precompile << "pghero/application.js"
app.config.assets.precompile << "pghero/application.css"
end
end
initializer "pghero.config" do
PgHero.config = Rails.application.config_for(:pghero) rescue {}
end
end
end
```
## Routes for Engines
```ruby
# config/routes.rb (in engine)
PgHero::Engine.routes.draw do
root to: "home#index"
resources :databases, only: [:show]
end
```
Mount in app:
```ruby
# config/routes.rb (in app)
mount PgHero::Engine, at: "pghero"
```
## YAML Configuration with ERB
For complex gems needing config files:
```ruby
def self.settings
@settings ||= begin
path = Rails.root.join("config", "blazer.yml")
if path.exist?
YAML.safe_load(ERB.new(File.read(path)).result, aliases: true)
else
{}
end
end
end
```
## Generator Pattern
```ruby
# lib/generators/gemname/install_generator.rb
module GemName
module Generators
class InstallGenerator < Rails::Generators::Base
source_root File.expand_path("templates", __dir__)
def copy_initializer
template "initializer.rb", "config/initializers/gemname.rb"
end
def copy_migration
migration_template "migration.rb", "db/migrate/create_gemname_tables.rb"
end
end
end
end
```
## Conditional Feature Detection
```ruby
# Check for specific Rails versions
if ActiveRecord.version >= Gem::Version.new("7.0")
# Rails 7+ specific code
end
# Check for optional dependencies
def self.client
@client ||= if defined?(OpenSearch::Client)
OpenSearch::Client.new
elsif defined?(Elasticsearch::Client)
Elasticsearch::Client.new
else
raise Error, "Install elasticsearch or opensearch-ruby"
end
end
```

View File

@@ -1,119 +0,0 @@
# Andrew Kane Resources
## Primary Documentation
- **Gem Patterns Article**: https://ankane.org/gem-patterns
- Kane's own documentation of patterns used across his gems
- Covers configuration, Rails integration, error handling
## Top Ruby Gems by Stars
### Search & Data
| Gem | Stars | Description | Source |
|-----|-------|-------------|--------|
| **Searchkick** | 6.6k+ | Intelligent search for Rails | https://github.com/ankane/searchkick |
| **Chartkick** | 6.4k+ | Beautiful charts in Ruby | https://github.com/ankane/chartkick |
| **Groupdate** | 3.8k+ | Group by day, week, month | https://github.com/ankane/groupdate |
| **Blazer** | 4.6k+ | SQL dashboard for Rails | https://github.com/ankane/blazer |
### Database & Migrations
| Gem | Stars | Description | Source |
|-----|-------|-------------|--------|
| **PgHero** | 8.2k+ | PostgreSQL insights | https://github.com/ankane/pghero |
| **Strong Migrations** | 4.1k+ | Safe migration checks | https://github.com/ankane/strong_migrations |
| **Dexter** | 1.8k+ | Auto index advisor | https://github.com/ankane/dexter |
| **PgSync** | 1.5k+ | Sync Postgres data | https://github.com/ankane/pgsync |
### Security & Encryption
| Gem | Stars | Description | Source |
|-----|-------|-------------|--------|
| **Lockbox** | 1.5k+ | Application-level encryption | https://github.com/ankane/lockbox |
| **Blind Index** | 1.0k+ | Encrypted search | https://github.com/ankane/blind_index |
| **Secure Headers** | — | Contributed patterns | Referenced in gems |
### Analytics & ML
| Gem | Stars | Description | Source |
|-----|-------|-------------|--------|
| **Ahoy** | 4.2k+ | Analytics for Rails | https://github.com/ankane/ahoy |
| **Neighbor** | 1.1k+ | Vector search for Rails | https://github.com/ankane/neighbor |
| **Rover** | 700+ | DataFrames for Ruby | https://github.com/ankane/rover |
| **Tomoto** | 200+ | Topic modeling | https://github.com/ankane/tomoto-ruby |
### Utilities
| Gem | Stars | Description | Source |
|-----|-------|-------------|--------|
| **Pretender** | 2.0k+ | Login as another user | https://github.com/ankane/pretender |
| **Authtrail** | 900+ | Login activity tracking | https://github.com/ankane/authtrail |
| **Notable** | 200+ | Track notable requests | https://github.com/ankane/notable |
| **Logstop** | 200+ | Filter sensitive logs | https://github.com/ankane/logstop |
## Key Source Files to Study
### Entry Point Patterns
- https://github.com/ankane/searchkick/blob/master/lib/searchkick.rb
- https://github.com/ankane/pghero/blob/master/lib/pghero.rb
- https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb
- https://github.com/ankane/lockbox/blob/master/lib/lockbox.rb
### Class Macro Implementations
- https://github.com/ankane/searchkick/blob/master/lib/searchkick/model.rb
- https://github.com/ankane/lockbox/blob/master/lib/lockbox/model.rb
- https://github.com/ankane/neighbor/blob/master/lib/neighbor/model.rb
- https://github.com/ankane/blind_index/blob/master/lib/blind_index/model.rb
### Rails Integration (Railtie/Engine)
- https://github.com/ankane/pghero/blob/master/lib/pghero/engine.rb
- https://github.com/ankane/searchkick/blob/master/lib/searchkick/railtie.rb
- https://github.com/ankane/ahoy/blob/master/lib/ahoy/engine.rb
- https://github.com/ankane/blazer/blob/master/lib/blazer/engine.rb
### Database Adapters
- https://github.com/ankane/strong_migrations/tree/master/lib/strong_migrations/adapters
- https://github.com/ankane/groupdate/tree/master/lib/groupdate/adapters
- https://github.com/ankane/neighbor/tree/master/lib/neighbor
### Error Messages (Template Pattern)
- https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations/error_messages.rb
### Gemspec Examples
- https://github.com/ankane/searchkick/blob/master/searchkick.gemspec
- https://github.com/ankane/neighbor/blob/master/neighbor.gemspec
- https://github.com/ankane/ahoy/blob/master/ahoy_matey.gemspec
### Test Setups
- https://github.com/ankane/searchkick/tree/master/test
- https://github.com/ankane/lockbox/tree/master/test
- https://github.com/ankane/strong_migrations/tree/master/test
## GitHub Profile
- **Profile**: https://github.com/ankane
- **All Ruby Repos**: https://github.com/ankane?tab=repositories&q=&type=&language=ruby&sort=stargazers
- **RubyGems Profile**: https://rubygems.org/profiles/ankane
## Blog Posts & Articles
- **ankane.org**: https://ankane.org/
- **Gem Patterns**: https://ankane.org/gem-patterns (essential reading)
- **Postgres Performance**: https://ankane.org/introducing-pghero
- **Search Tips**: https://ankane.org/search-rails
## Design Philosophy Summary
From studying 100+ gems, Kane's consistent principles:
1. **Zero dependencies when possible** - Each dep is a maintenance burden
2. **ActiveSupport.on_load always** - Never require Rails gems directly
3. **Class macro DSLs** - Single method configures everything
4. **Explicit over magic** - No method_missing, define methods directly
5. **Minitest only** - Simple, sufficient, no RSpec
6. **Multi-version testing** - Support broad Rails/Ruby versions
7. **Helpful errors** - Template-based messages with fix suggestions
8. **Abstract adapters** - Clean multi-database support
9. **Engine isolation** - isolate_namespace for mountable gems
10. **Minimal documentation** - Code is self-documenting, README is examples

View File

@@ -1,261 +0,0 @@
# Testing Patterns
## Minitest Setup
Kane exclusively uses Minitest—never RSpec.
```ruby
# test/test_helper.rb
require "bundler/setup"
Bundler.require(:default)
require "minitest/autorun"
require "minitest/pride"
# Load the gem
require "gemname"
# Test database setup (if needed)
ActiveRecord::Base.establish_connection(
adapter: "postgresql",
database: "gemname_test"
)
# Base test class
class Minitest::Test
def setup
# Reset state before each test
end
end
```
## Test File Structure
```ruby
# test/model_test.rb
require_relative "test_helper"
class ModelTest < Minitest::Test
def setup
User.delete_all
end
def test_basic_functionality
user = User.create!(email: "test@example.org")
assert_equal "test@example.org", user.email
end
def test_with_invalid_input
error = assert_raises(ArgumentError) do
User.create!(email: nil)
end
assert_match /email/, error.message
end
def test_class_method
result = User.search("test")
assert_kind_of Array, result
end
end
```
## Multi-Version Testing
Test against multiple Rails/Ruby versions using gemfiles:
```
test/
├── test_helper.rb
└── gemfiles/
├── activerecord70.gemfile
├── activerecord71.gemfile
└── activerecord72.gemfile
```
```ruby
# test/gemfiles/activerecord70.gemfile
source "https://rubygems.org"
gemspec path: "../../"
gem "activerecord", "~> 7.0.0"
gem "sqlite3"
```
```ruby
# test/gemfiles/activerecord72.gemfile
source "https://rubygems.org"
gemspec path: "../../"
gem "activerecord", "~> 7.2.0"
gem "sqlite3"
```
Run with specific gemfile:
```bash
BUNDLE_GEMFILE=test/gemfiles/activerecord70.gemfile bundle install
BUNDLE_GEMFILE=test/gemfiles/activerecord70.gemfile bundle exec rake test
```
## Rakefile
```ruby
# Rakefile
require "bundler/gem_tasks"
require "rake/testtask"
Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.pattern = "test/**/*_test.rb"
end
task default: :test
```
## GitHub Actions CI
```yaml
# .github/workflows/build.yml
name: build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- ruby: "3.2"
gemfile: activerecord70
- ruby: "3.3"
gemfile: activerecord71
- ruby: "3.3"
gemfile: activerecord72
env:
BUNDLE_GEMFILE: test/gemfiles/${{ matrix.gemfile }}.gemfile
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true
- run: bundle exec rake test
```
## Database-Specific Testing
```yaml
# .github/workflows/build.yml (with services)
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgres://postgres:postgres@localhost/gemname_test
```
## Test Database Setup
```ruby
# test/test_helper.rb
require "active_record"
# Connect to database
ActiveRecord::Base.establish_connection(
ENV["DATABASE_URL"] || {
adapter: "postgresql",
database: "gemname_test"
}
)
# Create tables
ActiveRecord::Schema.define do
create_table :users, force: true do |t|
t.string :email
t.text :encrypted_data
t.timestamps
end
end
# Define models
class User < ActiveRecord::Base
gemname_feature :email
end
```
## Assertion Patterns
```ruby
# Basic assertions
assert result
assert_equal expected, actual
assert_nil value
assert_empty array
# Exception testing
assert_raises(ArgumentError) { bad_code }
error = assert_raises(GemName::Error) do
risky_operation
end
assert_match /expected message/, error.message
# Refutations
refute condition
refute_equal unexpected, actual
refute_nil value
```
## Test Helpers
```ruby
# test/test_helper.rb
class Minitest::Test
def with_options(options)
original = GemName.options.dup
GemName.options.merge!(options)
yield
ensure
GemName.options = original
end
def assert_queries(expected_count)
queries = []
callback = ->(*, payload) { queries << payload[:sql] }
ActiveSupport::Notifications.subscribe("sql.active_record", callback)
yield
assert_equal expected_count, queries.size, "Expected #{expected_count} queries, got #{queries.size}"
ensure
ActiveSupport::Notifications.unsubscribe(callback)
end
end
```
## Skipping Tests
```ruby
def test_postgresql_specific
skip "PostgreSQL only" unless postgresql?
# test code
end
def postgresql?
ActiveRecord::Base.connection.adapter_name =~ /postg/i
end
```

View File

@@ -1,185 +0,0 @@
---
name: dhh-rails-style
description: This skill should be used when writing Ruby and Rails code in DHH's distinctive 37signals style. It applies when writing Ruby code, Rails applications, creating models, controllers, or any Ruby file. Triggers on Ruby/Rails code generation, refactoring requests, code review, or when the user mentions DHH, 37signals, Basecamp, HEY, or Campfire style. Embodies REST purity, fat models, thin controllers, Current attributes, Hotwire patterns, and the "clarity over cleverness" philosophy.
---
<objective>
Apply 37signals/DHH Rails conventions to Ruby and Rails code. This skill provides comprehensive domain expertise extracted from analyzing production 37signals codebases (Fizzy/Campfire) and DHH's code review patterns.
</objective>
<essential_principles>
## Core Philosophy
"The best code is the code you don't write. The second best is the code that's obviously correct."
**Vanilla Rails is plenty:**
- Rich domain models over service objects
- CRUD controllers over custom actions
- Concerns for horizontal code sharing
- Records as state instead of boolean columns
- Database-backed everything (no Redis)
- Build solutions before reaching for gems
**What they deliberately avoid:**
- devise (custom ~150-line auth instead)
- pundit/cancancan (simple role checks in models)
- sidekiq (Solid Queue uses database)
- redis (database for everything)
- view_component (partials work fine)
- GraphQL (REST with Turbo sufficient)
- factory_bot (fixtures are simpler)
- rspec (Minitest ships with Rails)
- Tailwind (native CSS with layers)
**Development Philosophy:**
- Ship, Validate, Refine - prototype-quality code to production to learn
- Fix root causes, not symptoms
- Write-time operations over read-time computations
- Database constraints over ActiveRecord validations
</essential_principles>
<intake>
What are you working on?
1. **Controllers** - REST mapping, concerns, Turbo responses, API patterns
2. **Models** - Concerns, state records, callbacks, scopes, POROs
3. **Views & Frontend** - Turbo, Stimulus, CSS, partials
4. **Architecture** - Routing, multi-tenancy, authentication, jobs, caching
5. **Testing** - Minitest, fixtures, integration tests
6. **Gems & Dependencies** - What to use vs avoid
7. **Code Review** - Review code against DHH style
8. **General Guidance** - Philosophy and conventions
**Specify a number or describe your task.**
</intake>
<routing>
| Response | Reference to Read |
|----------|-------------------|
| 1, controller | [controllers.md](./references/controllers.md) |
| 2, model | [models.md](./references/models.md) |
| 3, view, frontend, turbo, stimulus, css | [frontend.md](./references/frontend.md) |
| 4, architecture, routing, auth, job, cache | [architecture.md](./references/architecture.md) |
| 5, test, testing, minitest, fixture | [testing.md](./references/testing.md) |
| 6, gem, dependency, library | [gems.md](./references/gems.md) |
| 7, review | Read all references, then review code |
| 8, general task | Read relevant references based on context |
**After reading relevant references, apply patterns to the user's code.**
</routing>
<quick_reference>
## Naming Conventions
**Verbs:** `card.close`, `card.gild`, `board.publish` (not `set_style` methods)
**Predicates:** `card.closed?`, `card.golden?` (derived from presence of related record)
**Concerns:** Adjectives describing capability (`Closeable`, `Publishable`, `Watchable`)
**Controllers:** Nouns matching resources (`Cards::ClosuresController`)
**Scopes:**
- `chronologically`, `reverse_chronologically`, `alphabetically`, `latest`
- `preloaded` (standard eager loading name)
- `indexed_by`, `sorted_by` (parameterized)
- `active`, `unassigned` (business terms, not SQL-ish)
## REST Mapping
Instead of custom actions, create new resources:
```
POST /cards/:id/close → POST /cards/:id/closure
DELETE /cards/:id/close → DELETE /cards/:id/closure
POST /cards/:id/archive → POST /cards/:id/archival
```
## Ruby Syntax Preferences
```ruby
# Symbol arrays with spaces inside brackets
before_action :set_message, only: %i[ show edit update destroy ]
# Private method indentation
private
def set_message
@message = Message.find(params[:id])
end
# Expression-less case for conditionals
case
when params[:before].present?
messages.page_before(params[:before])
else
messages.last_page
end
# Bang methods for fail-fast
@message = Message.create!(params)
# Ternaries for simple conditionals
@room.direct? ? @room.users : @message.mentionees
```
## Key Patterns
**State as Records:**
```ruby
Card.joins(:closure) # closed cards
Card.where.missing(:closure) # open cards
```
**Current Attributes:**
```ruby
belongs_to :creator, default: -> { Current.user }
```
**Authorization on Models:**
```ruby
class User < ApplicationRecord
def can_administer?(message)
message.creator == self || admin?
end
end
```
</quick_reference>
<reference_index>
## Domain Knowledge
All detailed patterns in `references/`:
| File | Topics |
|------|--------|
| [controllers.md](./references/controllers.md) | REST mapping, concerns, Turbo responses, API patterns, HTTP caching |
| [models.md](./references/models.md) | Concerns, state records, callbacks, scopes, POROs, authorization, broadcasting |
| [frontend.md](./references/frontend.md) | Turbo Streams, Stimulus controllers, CSS layers, OKLCH colors, partials |
| [architecture.md](./references/architecture.md) | Routing, authentication, jobs, Current attributes, caching, database patterns |
| [testing.md](./references/testing.md) | Minitest, fixtures, unit/integration/system tests, testing patterns |
| [gems.md](./references/gems.md) | What they use vs avoid, decision framework, Gemfile examples |
</reference_index>
<success_criteria>
Code follows DHH style when:
- Controllers map to CRUD verbs on resources
- Models use concerns for horizontal behavior
- State is tracked via records, not booleans
- No unnecessary service objects or abstractions
- Database-backed solutions preferred over external services
- Tests use Minitest with fixtures
- Turbo/Stimulus for interactivity (no heavy JS frameworks)
- Native CSS with modern features (layers, OKLCH, nesting)
- Authorization logic lives on User model
- Jobs are shallow wrappers calling model methods
</success_criteria>
<credits>
Based on [The Unofficial 37signals/DHH Rails Style Guide](https://github.com/marckohlbrugge/unofficial-37signals-coding-style-guide) by [Marc Köhlbrugge](https://x.com/marckohlbrugge), generated through deep analysis of 265 pull requests from the Fizzy codebase.
**Important Disclaimers:**
- LLM-generated guide - may contain inaccuracies
- Code examples from Fizzy are licensed under the O'Saasy License
- Not affiliated with or endorsed by 37signals
</credits>

View File

@@ -1,653 +0,0 @@
# Architecture - DHH Rails Style
<routing>
## Routing
Everything maps to CRUD. Nested resources for related actions:
```ruby
Rails.application.routes.draw do
resources :boards do
resources :cards do
resource :closure
resource :goldness
resource :not_now
resources :assignments
resources :comments
end
end
end
```
**Verb-to-noun conversion:**
| Action | Resource |
|--------|----------|
| close a card | `card.closure` |
| watch a board | `board.watching` |
| mark as golden | `card.goldness` |
| archive a card | `card.archival` |
**Shallow nesting** - avoid deep URLs:
```ruby
resources :boards do
resources :cards, shallow: true # /boards/:id/cards, but /cards/:id
end
```
**Singular resources** for one-per-parent:
```ruby
resource :closure # not resources
resource :goldness
```
**Resolve for URL generation:**
```ruby
# config/routes.rb
resolve("Comment") { |comment| [comment.card, anchor: dom_id(comment)] }
# Now url_for(@comment) works correctly
```
</routing>
<multi_tenancy>
## Multi-Tenancy (Path-Based)
**Middleware extracts tenant** from URL prefix:
```ruby
# lib/tenant_extractor.rb
class TenantExtractor
def initialize(app)
@app = app
end
def call(env)
path = env["PATH_INFO"]
if match = path.match(%r{^/(\d+)(/.*)?$})
env["SCRIPT_NAME"] = "/#{match[1]}"
env["PATH_INFO"] = match[2] || "/"
end
@app.call(env)
end
end
```
**Cookie scoping** per tenant:
```ruby
# Cookies scoped to tenant path
cookies.signed[:session_id] = {
value: session.id,
path: "/#{Current.account.id}"
}
```
**Background job context** - serialize tenant:
```ruby
class ApplicationJob < ActiveJob::Base
around_perform do |job, block|
Current.set(account: job.arguments.first.account) { block.call }
end
end
```
**Recurring jobs** must iterate all tenants:
```ruby
class DailyDigestJob < ApplicationJob
def perform
Account.find_each do |account|
Current.set(account: account) do
send_digest_for(account)
end
end
end
end
```
**Controller security** - always scope through tenant:
```ruby
# Good - scoped through user's accessible records
@card = Current.user.accessible_cards.find(params[:id])
# Avoid - direct lookup
@card = Card.find(params[:id])
```
</multi_tenancy>
<authentication>
## Authentication
Custom passwordless magic link auth (~150 lines total):
```ruby
# app/models/session.rb
class Session < ApplicationRecord
belongs_to :user
before_create { self.token = SecureRandom.urlsafe_base64(32) }
end
# app/models/magic_link.rb
class MagicLink < ApplicationRecord
belongs_to :user
before_create do
self.code = SecureRandom.random_number(100_000..999_999).to_s
self.expires_at = 15.minutes.from_now
end
def expired?
expires_at < Time.current
end
end
```
**Why not Devise:**
- ~150 lines vs massive dependency
- No password storage liability
- Simpler UX for users
- Full control over flow
**Bearer token** for APIs:
```ruby
module Authentication
extend ActiveSupport::Concern
included do
before_action :authenticate
end
private
def authenticate
if bearer_token = request.headers["Authorization"]&.split(" ")&.last
Current.session = Session.find_by(token: bearer_token)
else
Current.session = Session.find_by(id: cookies.signed[:session_id])
end
redirect_to login_path unless Current.session
end
end
```
</authentication>
<background_jobs>
## Background Jobs
Jobs are shallow wrappers calling model methods:
```ruby
class NotifyWatchersJob < ApplicationJob
def perform(card)
card.notify_watchers
end
end
```
**Naming convention:**
- `_later` suffix for async: `card.notify_watchers_later`
- `_now` suffix for immediate: `card.notify_watchers_now`
```ruby
module Watchable
def notify_watchers_later
NotifyWatchersJob.perform_later(self)
end
def notify_watchers_now
NotifyWatchersJob.perform_now(self)
end
def notify_watchers
watchers.each do |watcher|
WatcherMailer.notification(watcher, self).deliver_later
end
end
end
```
**Database-backed** with Solid Queue:
- No Redis required
- Same transactional guarantees as your data
- Simpler infrastructure
**Transaction safety:**
```ruby
# config/application.rb
config.active_job.enqueue_after_transaction_commit = true
```
**Error handling** by type:
```ruby
class DeliveryJob < ApplicationJob
# Transient errors - retry with backoff
retry_on Net::OpenTimeout, Net::ReadTimeout,
Resolv::ResolvError,
wait: :polynomially_longer
# Permanent errors - log and discard
discard_on Net::SMTPSyntaxError do |job, error|
Sentry.capture_exception(error, level: :info)
end
end
```
**Batch processing** with continuable:
```ruby
class ProcessCardsJob < ApplicationJob
include ActiveJob::Continuable
def perform
Card.in_batches.each_record do |card|
checkpoint! # Resume from here if interrupted
process(card)
end
end
end
```
</background_jobs>
<database_patterns>
## Database Patterns
**UUIDs as primary keys** (time-sortable UUIDv7):
```ruby
# migration
create_table :cards, id: :uuid do |t|
t.references :board, type: :uuid, foreign_key: true
end
```
Benefits: No ID enumeration, distributed-friendly, client-side generation.
**State as records** (not booleans):
```ruby
# Instead of closed: boolean
class Card::Closure < ApplicationRecord
belongs_to :card
belongs_to :creator, class_name: "User"
end
# Queries become joins
Card.joins(:closure) # closed
Card.where.missing(:closure) # open
```
**Hard deletes** - no soft delete:
```ruby
# Just destroy
card.destroy!
# Use events for history
card.record_event(:deleted, by: Current.user)
```
Simplifies queries, uses event logs for auditing.
**Counter caches** for performance:
```ruby
class Comment < ApplicationRecord
belongs_to :card, counter_cache: true
end
# card.comments_count available without query
```
**Account scoping** on every table:
```ruby
class Card < ApplicationRecord
belongs_to :account
default_scope { where(account: Current.account) }
end
```
</database_patterns>
<current_attributes>
## Current Attributes
Use `Current` for request-scoped state:
```ruby
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :session, :user, :account, :request_id
delegate :user, to: :session, allow_nil: true
def account=(account)
super
Time.zone = account&.time_zone || "UTC"
end
end
```
Set in controller:
```ruby
class ApplicationController < ActionController::Base
before_action :set_current_request
private
def set_current_request
Current.session = authenticated_session
Current.account = Account.find(params[:account_id])
Current.request_id = request.request_id
end
end
```
Use throughout app:
```ruby
class Card < ApplicationRecord
belongs_to :creator, default: -> { Current.user }
end
```
</current_attributes>
<caching>
## Caching
**HTTP caching** with ETags:
```ruby
fresh_when etag: [@card, Current.user.timezone]
```
**Fragment caching:**
```erb
<% cache card do %>
<%= render card %>
<% end %>
```
**Russian doll caching:**
```erb
<% cache @board do %>
<% @board.cards.each do |card| %>
<% cache card do %>
<%= render card %>
<% end %>
<% end %>
<% end %>
```
**Cache invalidation** via `touch: true`:
```ruby
class Card < ApplicationRecord
belongs_to :board, touch: true
end
```
**Solid Cache** - database-backed:
- No Redis required
- Consistent with application data
- Simpler infrastructure
</caching>
<configuration>
## Configuration
**ENV.fetch with defaults:**
```ruby
# config/application.rb
config.active_job.queue_adapter = ENV.fetch("QUEUE_ADAPTER", "solid_queue").to_sym
config.cache_store = ENV.fetch("CACHE_STORE", "solid_cache").to_sym
```
**Multiple databases:**
```yaml
# config/database.yml
production:
primary:
<<: *default
cable:
<<: *default
migrations_paths: db/cable_migrate
queue:
<<: *default
migrations_paths: db/queue_migrate
cache:
<<: *default
migrations_paths: db/cache_migrate
```
**Switch between SQLite and MySQL via ENV:**
```ruby
adapter = ENV.fetch("DATABASE_ADAPTER", "sqlite3")
```
**CSP extensible via ENV:**
```ruby
config.content_security_policy do |policy|
policy.default_src :self
policy.script_src :self, *ENV.fetch("CSP_SCRIPT_SRC", "").split(",")
end
```
</configuration>
<testing>
## Testing
**Minitest**, not RSpec:
```ruby
class CardTest < ActiveSupport::TestCase
test "closing a card creates a closure" do
card = cards(:one)
card.close
assert card.closed?
assert_not_nil card.closure
end
end
```
**Fixtures** instead of factories:
```yaml
# test/fixtures/cards.yml
one:
title: First Card
board: main
creator: alice
two:
title: Second Card
board: main
creator: bob
```
**Integration tests** for controllers:
```ruby
class CardsControllerTest < ActionDispatch::IntegrationTest
test "closing a card" do
card = cards(:one)
sign_in users(:alice)
post card_closure_path(card)
assert_response :success
assert card.reload.closed?
end
end
```
**Tests ship with features** - same commit, not TDD-first but together.
**Regression tests for security fixes** - always.
</testing>
<events>
## Event Tracking
Events are the single source of truth:
```ruby
class Event < ApplicationRecord
belongs_to :creator, class_name: "User"
belongs_to :eventable, polymorphic: true
serialize :particulars, coder: JSON
end
```
**Eventable concern:**
```ruby
module Eventable
extend ActiveSupport::Concern
included do
has_many :events, as: :eventable, dependent: :destroy
end
def record_event(action, particulars = {})
events.create!(
creator: Current.user,
action: action,
particulars: particulars
)
end
end
```
**Webhooks driven by events** - events are the canonical source.
</events>
<email_patterns>
## Email Patterns
**Multi-tenant URL helpers:**
```ruby
class ApplicationMailer < ActionMailer::Base
def default_url_options
options = super
if Current.account
options[:script_name] = "/#{Current.account.id}"
end
options
end
end
```
**Timezone-aware delivery:**
```ruby
class NotificationMailer < ApplicationMailer
def daily_digest(user)
Time.use_zone(user.timezone) do
@user = user
@digest = user.digest_for_today
mail(to: user.email, subject: "Daily Digest")
end
end
end
```
**Batch delivery:**
```ruby
emails = users.map { |user| NotificationMailer.digest(user) }
ActiveJob.perform_all_later(emails.map(&:deliver_later))
```
**One-click unsubscribe (RFC 8058):**
```ruby
class ApplicationMailer < ActionMailer::Base
after_action :set_unsubscribe_headers
private
def set_unsubscribe_headers
headers["List-Unsubscribe-Post"] = "List-Unsubscribe=One-Click"
headers["List-Unsubscribe"] = "<#{unsubscribe_url}>"
end
end
```
</email_patterns>
<security_patterns>
## Security Patterns
**XSS prevention** - escape in helpers:
```ruby
def formatted_content(text)
# Escape first, then mark safe
simple_format(h(text)).html_safe
end
```
**SSRF protection:**
```ruby
# Resolve DNS once, pin the IP
def fetch_safely(url)
uri = URI.parse(url)
ip = Resolv.getaddress(uri.host)
# Block private networks
raise "Private IP" if private_ip?(ip)
# Use pinned IP for request
Net::HTTP.start(uri.host, uri.port, ipaddr: ip) { |http| ... }
end
def private_ip?(ip)
ip.start_with?("127.", "10.", "192.168.") ||
ip.match?(/^172\.(1[6-9]|2[0-9]|3[0-1])\./)
end
```
**Content Security Policy:**
```ruby
# config/initializers/content_security_policy.rb
Rails.application.configure do
config.content_security_policy do |policy|
policy.default_src :self
policy.script_src :self
policy.style_src :self, :unsafe_inline
policy.base_uri :none
policy.form_action :self
policy.frame_ancestors :self
end
end
```
**ActionText sanitization:**
```ruby
# config/initializers/action_text.rb
Rails.application.config.after_initialize do
ActionText::ContentHelper.allowed_tags = %w[
strong em a ul ol li p br h1 h2 h3 h4 blockquote
]
end
```
</security_patterns>
<active_storage>
## Active Storage Patterns
**Variant preprocessing:**
```ruby
class User < ApplicationRecord
has_one_attached :avatar do |attachable|
attachable.variant :thumb, resize_to_limit: [100, 100], preprocessed: true
attachable.variant :medium, resize_to_limit: [300, 300], preprocessed: true
end
end
```
**Direct upload expiry** - extend for slow connections:
```ruby
# config/initializers/active_storage.rb
Rails.application.config.active_storage.service_urls_expire_in = 48.hours
```
**Avatar optimization** - redirect to blob:
```ruby
def show
expires_in 1.year, public: true
redirect_to @user.avatar.variant(:thumb).processed.url, allow_other_host: true
end
```
**Mirror service** for migrations:
```yaml
# config/storage.yml
production:
service: Mirror
primary: amazon
mirrors: [google]
```
</active_storage>

View File

@@ -1,303 +0,0 @@
# Controllers - DHH Rails Style
<rest_mapping>
## Everything Maps to CRUD
Custom actions become new resources. Instead of verbs on existing resources, create noun resources:
```ruby
# Instead of this:
POST /cards/:id/close
DELETE /cards/:id/close
POST /cards/:id/archive
# Do this:
POST /cards/:id/closure # create closure
DELETE /cards/:id/closure # destroy closure
POST /cards/:id/archival # create archival
```
**Real examples from 37signals:**
```ruby
resources :cards do
resource :closure # closing/reopening
resource :goldness # marking important
resource :not_now # postponing
resources :assignments # managing assignees
end
```
Each resource gets its own controller with standard CRUD actions.
</rest_mapping>
<controller_concerns>
## Concerns for Shared Behavior
Controllers use concerns extensively. Common patterns:
**CardScoped** - loads @card, @board, provides render_card_replacement
```ruby
module CardScoped
extend ActiveSupport::Concern
included do
before_action :set_card
end
private
def set_card
@card = Card.find(params[:card_id])
@board = @card.board
end
def render_card_replacement
render turbo_stream: turbo_stream.replace(@card)
end
end
```
**BoardScoped** - loads @board
**CurrentRequest** - populates Current with request data
**CurrentTimezone** - wraps requests in user's timezone
**FilterScoped** - handles complex filtering
**TurboFlash** - flash messages via Turbo Stream
**ViewTransitions** - disables on page refresh
**BlockSearchEngineIndexing** - sets X-Robots-Tag header
**RequestForgeryProtection** - Sec-Fetch-Site CSRF (modern browsers)
</controller_concerns>
<authorization_patterns>
## Authorization Patterns
Controllers check permissions via before_action, models define what permissions mean:
```ruby
# Controller concern
module Authorization
extend ActiveSupport::Concern
private
def ensure_can_administer
head :forbidden unless Current.user.admin?
end
def ensure_is_staff_member
head :forbidden unless Current.user.staff?
end
end
# Usage
class BoardsController < ApplicationController
before_action :ensure_can_administer, only: [:destroy]
end
```
**Model-level authorization:**
```ruby
class Board < ApplicationRecord
def editable_by?(user)
user.admin? || user == creator
end
def publishable_by?(user)
editable_by?(user) && !published?
end
end
```
Keep authorization simple, readable, colocated with domain.
</authorization_patterns>
<security_concerns>
## Security Concerns
**Sec-Fetch-Site CSRF Protection:**
Modern browsers send Sec-Fetch-Site header. Use it for defense in depth:
```ruby
module RequestForgeryProtection
extend ActiveSupport::Concern
included do
before_action :verify_request_origin
end
private
def verify_request_origin
return if request.get? || request.head?
return if %w[same-origin same-site].include?(
request.headers["Sec-Fetch-Site"]&.downcase
)
# Fall back to token verification for older browsers
verify_authenticity_token
end
end
```
**Rate Limiting (Rails 8+):**
```ruby
class MagicLinksController < ApplicationController
rate_limit to: 10, within: 15.minutes, only: :create
end
```
Apply to: auth endpoints, email sending, external API calls, resource creation.
</security_concerns>
<request_context>
## Request Context Concerns
**CurrentRequest** - populates Current with HTTP metadata:
```ruby
module CurrentRequest
extend ActiveSupport::Concern
included do
before_action :set_current_request
end
private
def set_current_request
Current.request_id = request.request_id
Current.user_agent = request.user_agent
Current.ip_address = request.remote_ip
Current.referrer = request.referrer
end
end
```
**CurrentTimezone** - wraps requests in user's timezone:
```ruby
module CurrentTimezone
extend ActiveSupport::Concern
included do
around_action :set_timezone
helper_method :timezone_from_cookie
end
private
def set_timezone
Time.use_zone(timezone_from_cookie) { yield }
end
def timezone_from_cookie
cookies[:timezone] || "UTC"
end
end
```
**SetPlatform** - detects mobile/desktop:
```ruby
module SetPlatform
extend ActiveSupport::Concern
included do
helper_method :platform
end
def platform
@platform ||= request.user_agent&.match?(/Mobile|Android/) ? :mobile : :desktop
end
end
```
</request_context>
<turbo_responses>
## Turbo Stream Responses
Use Turbo Streams for partial updates:
```ruby
class Cards::ClosuresController < ApplicationController
include CardScoped
def create
@card.close
render_card_replacement
end
def destroy
@card.reopen
render_card_replacement
end
end
```
For complex updates, use morphing:
```ruby
render turbo_stream: turbo_stream.morph(@card)
```
</turbo_responses>
<api_patterns>
## API Design
Same controllers, different format. Convention for responses:
```ruby
def create
@card = Card.create!(card_params)
respond_to do |format|
format.html { redirect_to @card }
format.json { head :created, location: @card }
end
end
def update
@card.update!(card_params)
respond_to do |format|
format.html { redirect_to @card }
format.json { head :no_content }
end
end
def destroy
@card.destroy
respond_to do |format|
format.html { redirect_to cards_path }
format.json { head :no_content }
end
end
```
**Status codes:**
- Create: 201 Created + Location header
- Update: 204 No Content
- Delete: 204 No Content
- Bearer token authentication
</api_patterns>
<http_caching>
## HTTP Caching
Extensive use of ETags and conditional GETs:
```ruby
class CardsController < ApplicationController
def show
@card = Card.find(params[:id])
fresh_when etag: [@card, Current.user.timezone]
end
def index
@cards = @board.cards.preloaded
fresh_when etag: [@cards, @board.updated_at]
end
end
```
Key insight: Times render server-side in user's timezone, so timezone must affect the ETag to prevent serving wrong times to other timezones.
**ApplicationController global etag:**
```ruby
class ApplicationController < ActionController::Base
etag { "v1" } # Bump to invalidate all caches
end
```
Use `touch: true` on associations for cache invalidation.
</http_caching>

View File

@@ -1,510 +0,0 @@
# Frontend - DHH Rails Style
<turbo_patterns>
## Turbo Patterns
**Turbo Streams** for partial updates:
```erb
<%# app/views/cards/closures/create.turbo_stream.erb %>
<%= turbo_stream.replace @card %>
```
**Morphing** for complex updates:
```ruby
render turbo_stream: turbo_stream.morph(@card)
```
**Global morphing** - enable in layout:
```ruby
turbo_refreshes_with method: :morph, scroll: :preserve
```
**Fragment caching** with `cached: true`:
```erb
<%= render partial: "card", collection: @cards, cached: true %>
```
**No ViewComponents** - standard partials work fine.
</turbo_patterns>
<turbo_morphing>
## Turbo Morphing Best Practices
**Listen for morph events** to restore client state:
```javascript
document.addEventListener("turbo:morph-element", (event) => {
// Restore any client-side state after morph
})
```
**Permanent elements** - skip morphing with data attribute:
```erb
<div data-turbo-permanent id="notification-count">
<%= @count %>
</div>
```
**Frame morphing** - add refresh attribute:
```erb
<%= turbo_frame_tag :assignment, src: path, refresh: :morph %>
```
**Common issues and solutions:**
| Problem | Solution |
|---------|----------|
| Timers not updating | Clear/restart in morph event listener |
| Forms resetting | Wrap form sections in turbo frames |
| Pagination breaking | Use turbo frames with `refresh: :morph` |
| Flickering on replace | Switch to morph instead of replace |
| localStorage loss | Listen to `turbo:morph-element`, restore state |
</turbo_morphing>
<turbo_frames>
## Turbo Frames
**Lazy loading** with spinner:
```erb
<%= turbo_frame_tag "menu",
src: menu_path,
loading: :lazy do %>
<div class="spinner">Loading...</div>
<% end %>
```
**Inline editing** with edit/view toggle:
```erb
<%= turbo_frame_tag dom_id(card, :edit) do %>
<%= link_to "Edit", edit_card_path(card),
data: { turbo_frame: dom_id(card, :edit) } %>
<% end %>
```
**Target parent frame** without hardcoding:
```erb
<%= form_with model: @card, data: { turbo_frame: "_parent" } do |f| %>
```
**Real-time subscriptions:**
```erb
<%= turbo_stream_from @card %>
<%= turbo_stream_from @card, :activity %>
```
</turbo_frames>
<stimulus_controllers>
## Stimulus Controllers
52 controllers in Fizzy, split 62% reusable, 38% domain-specific.
**Characteristics:**
- Single responsibility per controller
- Configuration via values/classes
- Events for communication
- Private methods with #
- Most under 50 lines
**Examples:**
```javascript
// copy-to-clipboard (25 lines)
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = { content: String }
copy() {
navigator.clipboard.writeText(this.contentValue)
this.#showFeedback()
}
#showFeedback() {
this.element.classList.add("copied")
setTimeout(() => this.element.classList.remove("copied"), 1500)
}
}
```
```javascript
// auto-click (7 lines)
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
this.element.click()
}
}
```
```javascript
// toggle-class (31 lines)
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static classes = ["toggle"]
static values = { open: { type: Boolean, default: false } }
toggle() {
this.openValue = !this.openValue
}
openValueChanged() {
this.element.classList.toggle(this.toggleClass, this.openValue)
}
}
```
```javascript
// auto-submit (28 lines) - debounced form submission
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = { delay: { type: Number, default: 300 } }
connect() {
this.timeout = null
}
submit() {
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.element.requestSubmit()
}, this.delayValue)
}
disconnect() {
clearTimeout(this.timeout)
}
}
```
```javascript
// dialog (45 lines) - native HTML dialog management
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
open() {
this.element.showModal()
}
close() {
this.element.close()
this.dispatch("closed")
}
clickOutside(event) {
if (event.target === this.element) this.close()
}
}
```
```javascript
// local-time (40 lines) - relative time display
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = { datetime: String }
connect() {
this.#updateTime()
}
#updateTime() {
const date = new Date(this.datetimeValue)
const now = new Date()
const diffMinutes = Math.floor((now - date) / 60000)
if (diffMinutes < 60) {
this.element.textContent = `${diffMinutes}m ago`
} else if (diffMinutes < 1440) {
this.element.textContent = `${Math.floor(diffMinutes / 60)}h ago`
} else {
this.element.textContent = `${Math.floor(diffMinutes / 1440)}d ago`
}
}
}
```
</stimulus_controllers>
<stimulus_best_practices>
## Stimulus Best Practices
**Values API** over getAttribute:
```javascript
// Good
static values = { delay: { type: Number, default: 300 } }
// Avoid
this.element.getAttribute("data-delay")
```
**Cleanup in disconnect:**
```javascript
disconnect() {
clearTimeout(this.timeout)
this.observer?.disconnect()
document.removeEventListener("keydown", this.boundHandler)
}
```
**Action filters** - `:self` prevents bubbling:
```erb
<div data-action="click->menu#toggle:self">
```
**Helper extraction** - shared utilities in separate modules:
```javascript
// app/javascript/helpers/timing.js
export function debounce(fn, delay) {
let timeout
return (...args) => {
clearTimeout(timeout)
timeout = setTimeout(() => fn(...args), delay)
}
}
```
**Event dispatching** for loose coupling:
```javascript
this.dispatch("selected", { detail: { id: this.idValue } })
```
</stimulus_best_practices>
<view_helpers>
## View Helpers (Stimulus-Integrated)
**Dialog helper:**
```ruby
def dialog_tag(id, &block)
tag.dialog(
id: id,
data: {
controller: "dialog",
action: "click->dialog#clickOutside keydown.esc->dialog#close"
},
&block
)
end
```
**Auto-submit form helper:**
```ruby
def auto_submit_form_with(model:, delay: 300, **options, &block)
form_with(
model: model,
data: {
controller: "auto-submit",
auto_submit_delay_value: delay,
action: "input->auto-submit#submit"
},
**options,
&block
)
end
```
**Copy button helper:**
```ruby
def copy_button(content:, label: "Copy")
tag.button(
label,
data: {
controller: "copy",
copy_content_value: content,
action: "click->copy#copy"
}
)
end
```
</view_helpers>
<css_architecture>
## CSS Architecture
Vanilla CSS with modern features, no preprocessors.
**CSS @layer** for cascade control:
```css
@layer reset, base, components, modules, utilities;
@layer reset {
*, *::before, *::after { box-sizing: border-box; }
}
@layer base {
body { font-family: var(--font-sans); }
}
@layer components {
.btn { /* button styles */ }
}
@layer modules {
.card { /* card module styles */ }
}
@layer utilities {
.hidden { display: none; }
}
```
**OKLCH color system** for perceptual uniformity:
```css
:root {
--color-primary: oklch(60% 0.15 250);
--color-success: oklch(65% 0.2 145);
--color-warning: oklch(75% 0.15 85);
--color-danger: oklch(55% 0.2 25);
}
```
**Dark mode** via CSS variables:
```css
:root {
--bg: oklch(98% 0 0);
--text: oklch(20% 0 0);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: oklch(15% 0 0);
--text: oklch(90% 0 0);
}
}
```
**Native CSS nesting:**
```css
.card {
padding: var(--space-4);
& .title {
font-weight: bold;
}
&:hover {
background: var(--bg-hover);
}
}
```
**~60 minimal utilities** vs Tailwind's hundreds.
**Modern features used:**
- `@starting-style` for enter animations
- `color-mix()` for color manipulation
- `:has()` for parent selection
- Logical properties (`margin-inline`, `padding-block`)
- Container queries
</css_architecture>
<view_patterns>
## View Patterns
**Standard partials** - no ViewComponents:
```erb
<%# app/views/cards/_card.html.erb %>
<article id="<%= dom_id(card) %>" class="card">
<%= render "cards/header", card: card %>
<%= render "cards/body", card: card %>
<%= render "cards/footer", card: card %>
</article>
```
**Fragment caching:**
```erb
<% cache card do %>
<%= render "cards/card", card: card %>
<% end %>
```
**Collection caching:**
```erb
<%= render partial: "card", collection: @cards, cached: true %>
```
**Simple component naming** - no strict BEM:
```css
.card { }
.card .title { }
.card .actions { }
.card.golden { }
.card.closed { }
```
</view_patterns>
<caching_with_personalization>
## User-Specific Content in Caches
Move personalization to client-side JavaScript to preserve caching:
```erb
<%# Cacheable fragment %>
<% cache card do %>
<article class="card"
data-creator-id="<%= card.creator_id %>"
data-controller="ownership"
data-ownership-current-user-value="<%= Current.user.id %>">
<button data-ownership-target="ownerOnly" class="hidden">Delete</button>
</article>
<% end %>
```
```javascript
// Reveal user-specific elements after cache hit
export default class extends Controller {
static values = { currentUser: Number }
static targets = ["ownerOnly"]
connect() {
const creatorId = parseInt(this.element.dataset.creatorId)
if (creatorId === this.currentUserValue) {
this.ownerOnlyTargets.forEach(el => el.classList.remove("hidden"))
}
}
}
```
**Extract dynamic content** to separate frames:
```erb
<% cache [card, board] do %>
<article class="card">
<%= turbo_frame_tag card, :assignment,
src: card_assignment_path(card),
refresh: :morph %>
</article>
<% end %>
```
Assignment dropdown updates independently without invalidating parent cache.
</caching_with_personalization>
<broadcasting>
## Broadcasting with Turbo Streams
**Model callbacks** for real-time updates:
```ruby
class Card < ApplicationRecord
include Broadcastable
after_create_commit :broadcast_created
after_update_commit :broadcast_updated
after_destroy_commit :broadcast_removed
private
def broadcast_created
broadcast_append_to [Current.account, board], :cards
end
def broadcast_updated
broadcast_replace_to [Current.account, board], :cards
end
def broadcast_removed
broadcast_remove_to [Current.account, board], :cards
end
end
```
**Scope by tenant** using `[Current.account, resource]` pattern.
</broadcasting>

View File

@@ -1,266 +0,0 @@
# Gems - DHH Rails Style
<what_they_use>
## What 37signals Uses
**Core Rails stack:**
- turbo-rails, stimulus-rails, importmap-rails
- propshaft (asset pipeline)
**Database-backed services (Solid suite):**
- solid_queue - background jobs
- solid_cache - caching
- solid_cable - WebSockets/Action Cable
**Authentication & Security:**
- bcrypt (for any password hashing needed)
**Their own gems:**
- geared_pagination (cursor-based pagination)
- lexxy (rich text editor)
- mittens (mailer utilities)
**Utilities:**
- rqrcode (QR code generation)
- redcarpet + rouge (Markdown rendering)
- web-push (push notifications)
**Deployment & Operations:**
- kamal (Docker deployment)
- thruster (HTTP/2 proxy)
- mission_control-jobs (job monitoring)
- autotuner (GC tuning)
</what_they_use>
<what_they_avoid>
## What They Deliberately Avoid
**Authentication:**
```
devise → Custom ~150-line auth
```
Why: Full control, no password liability with magic links, simpler.
**Authorization:**
```
pundit/cancancan → Simple role checks in models
```
Why: Most apps don't need policy objects. A method on the model suffices:
```ruby
class Board < ApplicationRecord
def editable_by?(user)
user.admin? || user == creator
end
end
```
**Background Jobs:**
```
sidekiq → Solid Queue
```
Why: Database-backed means no Redis, same transactional guarantees.
**Caching:**
```
redis → Solid Cache
```
Why: Database is already there, simpler infrastructure.
**Search:**
```
elasticsearch → Custom sharded search
```
Why: Built exactly what they need, no external service dependency.
**View Layer:**
```
view_component → Standard partials
```
Why: Partials work fine. ViewComponents add complexity without clear benefit for their use case.
**API:**
```
GraphQL → REST with Turbo
```
Why: REST is sufficient when you control both ends. GraphQL complexity not justified.
**Factories:**
```
factory_bot → Fixtures
```
Why: Fixtures are simpler, faster, and encourage thinking about data relationships upfront.
**Service Objects:**
```
Interactor, Trailblazer → Fat models
```
Why: Business logic stays in models. Methods like `card.close` instead of `CardCloser.call(card)`.
**Form Objects:**
```
Reform, dry-validation → params.expect + model validations
```
Why: Rails 7.1's `params.expect` is clean enough. Contextual validations on model.
**Decorators:**
```
Draper → View helpers + partials
```
Why: Helpers and partials are simpler. No decorator indirection.
**CSS:**
```
Tailwind, Sass → Native CSS
```
Why: Modern CSS has nesting, variables, layers. No build step needed.
**Frontend:**
```
React, Vue, SPAs → Turbo + Stimulus
```
Why: Server-rendered HTML with sprinkles of JS. SPA complexity not justified.
**Testing:**
```
RSpec → Minitest
```
Why: Simpler, faster boot, less DSL magic, ships with Rails.
</what_they_avoid>
<testing_philosophy>
## Testing Philosophy
**Minitest** - simpler, faster:
```ruby
class CardTest < ActiveSupport::TestCase
test "closing creates closure" do
card = cards(:one)
assert_difference -> { Card::Closure.count } do
card.close
end
assert card.closed?
end
end
```
**Fixtures** - loaded once, deterministic:
```yaml
# test/fixtures/cards.yml
open_card:
title: Open Card
board: main
creator: alice
closed_card:
title: Closed Card
board: main
creator: bob
```
**Dynamic timestamps** with ERB:
```yaml
recent:
title: Recent
created_at: <%= 1.hour.ago %>
old:
title: Old
created_at: <%= 1.month.ago %>
```
**Time travel** for time-dependent tests:
```ruby
test "expires after 15 minutes" do
magic_link = MagicLink.create!(user: users(:alice))
travel 16.minutes
assert magic_link.expired?
end
```
**VCR** for external APIs:
```ruby
VCR.use_cassette("stripe/charge") do
charge = Stripe::Charge.create(amount: 1000)
assert charge.paid
end
```
**Tests ship with features** - same commit, not before or after.
</testing_philosophy>
<decision_framework>
## Decision Framework
Before adding a gem, ask:
1. **Can vanilla Rails do this?**
- ActiveRecord can do most things Sequel can
- ActionMailer handles email fine
- ActiveJob works for most job needs
2. **Is the complexity worth it?**
- 150 lines of custom code vs. 10,000-line gem
- You'll understand your code better
- Fewer upgrade headaches
3. **Does it add infrastructure?**
- Redis? Consider database-backed alternatives
- External service? Consider building in-house
- Simpler infrastructure = fewer failure modes
4. **Is it from someone you trust?**
- 37signals gems: battle-tested at scale
- Well-maintained, focused gems: usually fine
- Kitchen-sink gems: probably overkill
**The philosophy:**
> "Build solutions before reaching for gems."
Not anti-gem, but pro-understanding. Use gems when they genuinely solve a problem you have, not a problem you might have.
</decision_framework>
<gem_patterns>
## Gem Usage Patterns
**Pagination:**
```ruby
# geared_pagination - cursor-based
class CardsController < ApplicationController
def index
@cards = @board.cards.geared(page: params[:page])
end
end
```
**Markdown:**
```ruby
# redcarpet + rouge
class MarkdownRenderer
def self.render(text)
Redcarpet::Markdown.new(
Redcarpet::Render::HTML.new(filter_html: true),
autolink: true,
fenced_code_blocks: true
).render(text)
end
end
```
**Background jobs:**
```ruby
# solid_queue - no Redis
class ApplicationJob < ActiveJob::Base
queue_as :default
# Just works, backed by database
end
```
**Caching:**
```ruby
# solid_cache - no Redis
# config/environments/production.rb
config.cache_store = :solid_cache_store
```
</gem_patterns>

View File

@@ -1,359 +0,0 @@
# Models - DHH Rails Style
<model_concerns>
## Concerns for Horizontal Behavior
Models heavily use concerns. A typical Card model includes 14+ concerns:
```ruby
class Card < ApplicationRecord
include Assignable
include Attachments
include Broadcastable
include Closeable
include Colored
include Eventable
include Golden
include Mentions
include Multistep
include Pinnable
include Postponable
include Readable
include Searchable
include Taggable
include Watchable
end
```
Each concern is self-contained with associations, scopes, and methods.
**Naming:** Adjectives describing capability (`Closeable`, `Publishable`, `Watchable`)
</model_concerns>
<state_records>
## State as Records, Not Booleans
Instead of boolean columns, create separate records:
```ruby
# Instead of:
closed: boolean
is_golden: boolean
postponed: boolean
# Create records:
class Card::Closure < ApplicationRecord
belongs_to :card
belongs_to :creator, class_name: "User"
end
class Card::Goldness < ApplicationRecord
belongs_to :card
belongs_to :creator, class_name: "User"
end
class Card::NotNow < ApplicationRecord
belongs_to :card
belongs_to :creator, class_name: "User"
end
```
**Benefits:**
- Automatic timestamps (when it happened)
- Track who made changes
- Easy filtering via joins and `where.missing`
- Enables rich UI showing when/who
**In the model:**
```ruby
module Closeable
extend ActiveSupport::Concern
included do
has_one :closure, dependent: :destroy
end
def closed?
closure.present?
end
def close(creator: Current.user)
create_closure!(creator: creator)
end
def reopen
closure&.destroy
end
end
```
**Querying:**
```ruby
Card.joins(:closure) # closed cards
Card.where.missing(:closure) # open cards
```
</state_records>
<callbacks>
## Callbacks - Used Sparingly
Only 38 callback occurrences across 30 files in Fizzy. Guidelines:
**Use for:**
- `after_commit` for async work
- `before_save` for derived data
- `after_create_commit` for side effects
**Avoid:**
- Complex callback chains
- Business logic in callbacks
- Synchronous external calls
```ruby
class Card < ApplicationRecord
after_create_commit :notify_watchers_later
before_save :update_search_index, if: :title_changed?
private
def notify_watchers_later
NotifyWatchersJob.perform_later(self)
end
end
```
</callbacks>
<scopes>
## Scope Naming
Standard scope names:
```ruby
class Card < ApplicationRecord
scope :chronologically, -> { order(created_at: :asc) }
scope :reverse_chronologically, -> { order(created_at: :desc) }
scope :alphabetically, -> { order(title: :asc) }
scope :latest, -> { reverse_chronologically.limit(10) }
# Standard eager loading
scope :preloaded, -> { includes(:creator, :assignees, :tags) }
# Parameterized
scope :indexed_by, ->(column) { order(column => :asc) }
scope :sorted_by, ->(column, direction = :asc) { order(column => direction) }
end
```
</scopes>
<poros>
## Plain Old Ruby Objects
POROs namespaced under parent models:
```ruby
# app/models/event/description.rb
class Event::Description
def initialize(event)
@event = event
end
def to_s
# Presentation logic for event description
end
end
# app/models/card/eventable/system_commenter.rb
class Card::Eventable::SystemCommenter
def initialize(card)
@card = card
end
def comment(message)
# Business logic
end
end
# app/models/user/filtering.rb
class User::Filtering
# View context bundling
end
```
**NOT used for service objects.** Business logic stays in models.
</poros>
<verbs_predicates>
## Method Naming
**Verbs** - Actions that change state:
```ruby
card.close
card.reopen
card.gild # make golden
card.ungild
board.publish
board.archive
```
**Predicates** - Queries derived from state:
```ruby
card.closed? # closure.present?
card.golden? # goldness.present?
board.published?
```
**Avoid** generic setters:
```ruby
# Bad
card.set_closed(true)
card.update_golden_status(false)
# Good
card.close
card.ungild
```
</verbs_predicates>
<validation_philosophy>
## Validation Philosophy
Minimal validations on models. Use contextual validations on form/operation objects:
```ruby
# Model - minimal
class User < ApplicationRecord
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
end
# Form object - contextual
class Signup
include ActiveModel::Model
attr_accessor :email, :name, :terms_accepted
validates :email, :name, presence: true
validates :terms_accepted, acceptance: true
def save
return false unless valid?
User.create!(email: email, name: name)
end
end
```
**Prefer database constraints** over model validations for data integrity:
```ruby
# migration
add_index :users, :email, unique: true
add_foreign_key :cards, :boards
```
</validation_philosophy>
<error_handling>
## Let It Crash Philosophy
Use bang methods that raise exceptions on failure:
```ruby
# Preferred - raises on failure
@card = Card.create!(card_params)
@card.update!(title: new_title)
@comment.destroy!
# Avoid - silent failures
@card = Card.create(card_params) # returns false on failure
if @card.save
# ...
end
```
Let errors propagate naturally. Rails handles ActiveRecord::RecordInvalid with 422 responses.
</error_handling>
<default_values>
## Default Values with Lambdas
Use lambda defaults for associations with Current:
```ruby
class Card < ApplicationRecord
belongs_to :creator, class_name: "User", default: -> { Current.user }
belongs_to :account, default: -> { Current.account }
end
class Comment < ApplicationRecord
belongs_to :commenter, class_name: "User", default: -> { Current.user }
end
```
Lambdas ensure dynamic resolution at creation time.
</default_values>
<rails_71_patterns>
## Rails 7.1+ Model Patterns
**Normalizes** - clean data before validation:
```ruby
class User < ApplicationRecord
normalizes :email, with: ->(email) { email.strip.downcase }
normalizes :phone, with: ->(phone) { phone.gsub(/\D/, "") }
end
```
**Delegated Types** - replace polymorphic associations:
```ruby
class Message < ApplicationRecord
delegated_type :messageable, types: %w[Comment Reply Announcement]
end
# Now you get:
message.comment? # true if Comment
message.comment # returns the Comment
Message.comments # scope for Comment messages
```
**Store Accessor** - structured JSON storage:
```ruby
class User < ApplicationRecord
store :settings, accessors: [:theme, :notifications_enabled], coder: JSON
end
user.theme = "dark"
user.notifications_enabled = true
```
</rails_71_patterns>
<concern_guidelines>
## Concern Guidelines
- **50-150 lines** per concern (most are ~100)
- **Cohesive** - related functionality only
- **Named for capabilities** - `Closeable`, `Watchable`, not `CardHelpers`
- **Self-contained** - associations, scopes, methods together
- **Not for mere organization** - create when genuine reuse needed
**Touch chains** for cache invalidation:
```ruby
class Comment < ApplicationRecord
belongs_to :card, touch: true
end
class Card < ApplicationRecord
belongs_to :board, touch: true
end
```
When comment updates, card's `updated_at` changes, which cascades to board.
**Transaction wrapping** for related updates:
```ruby
class Card < ApplicationRecord
def close(creator: Current.user)
transaction do
create_closure!(creator: creator)
record_event(:closed)
notify_watchers_later
end
end
end
```
</concern_guidelines>

View File

@@ -1,338 +0,0 @@
# Testing - DHH Rails Style
## Core Philosophy
"Minitest with fixtures - simple, fast, deterministic." The approach prioritizes pragmatism over convention.
## Why Minitest Over RSpec
- **Simpler**: Less DSL magic, plain Ruby assertions
- **Ships with Rails**: No additional dependencies
- **Faster boot times**: Less overhead
- **Plain Ruby**: No specialized syntax to learn
## Fixtures as Test Data
Rather than factories, fixtures provide preloaded data:
- Loaded once, reused across tests
- No runtime object creation overhead
- Explicit relationship visibility
- Deterministic IDs for easier debugging
### Fixture Structure
```yaml
# test/fixtures/users.yml
david:
identity: david
account: basecamp
role: admin
jason:
identity: jason
account: basecamp
role: member
# test/fixtures/rooms.yml
watercooler:
name: Water Cooler
creator: david
direct: false
# test/fixtures/messages.yml
greeting:
body: Hello everyone!
room: watercooler
creator: david
```
### Using Fixtures in Tests
```ruby
test "sending a message" do
user = users(:david)
room = rooms(:watercooler)
# Test with fixture data
end
```
### Dynamic Fixture Values
ERB enables time-sensitive data:
```yaml
recent_card:
title: Recent Card
created_at: <%= 1.hour.ago %>
old_card:
title: Old Card
created_at: <%= 1.month.ago %>
```
## Test Organization
### Unit Tests
Verify business logic using setup blocks and standard assertions:
```ruby
class CardTest < ActiveSupport::TestCase
setup do
@card = cards(:one)
@user = users(:david)
end
test "closing a card creates a closure" do
assert_difference -> { Card::Closure.count } do
@card.close(creator: @user)
end
assert @card.closed?
assert_equal @user, @card.closure.creator
end
test "reopening a card destroys the closure" do
@card.close(creator: @user)
assert_difference -> { Card::Closure.count }, -1 do
@card.reopen
end
refute @card.closed?
end
end
```
### Integration Tests
Test full request/response cycles:
```ruby
class CardsControllerTest < ActionDispatch::IntegrationTest
setup do
@user = users(:david)
sign_in @user
end
test "closing a card" do
card = cards(:one)
post card_closure_path(card)
assert_response :success
assert card.reload.closed?
end
test "unauthorized user cannot close card" do
sign_in users(:guest)
card = cards(:one)
post card_closure_path(card)
assert_response :forbidden
refute card.reload.closed?
end
end
```
### System Tests
Browser-based tests using Capybara:
```ruby
class MessagesTest < ApplicationSystemTestCase
test "sending a message" do
sign_in users(:david)
visit room_path(rooms(:watercooler))
fill_in "Message", with: "Hello, world!"
click_button "Send"
assert_text "Hello, world!"
end
test "editing own message" do
sign_in users(:david)
visit room_path(rooms(:watercooler))
within "#message_#{messages(:greeting).id}" do
click_on "Edit"
end
fill_in "Message", with: "Updated message"
click_button "Save"
assert_text "Updated message"
end
test "drag and drop card to new column" do
sign_in users(:david)
visit board_path(boards(:main))
card = find("#card_#{cards(:one).id}")
target = find("#column_#{columns(:done).id}")
card.drag_to target
assert_selector "#column_#{columns(:done).id} #card_#{cards(:one).id}"
end
end
```
## Advanced Patterns
### Time Testing
Use `travel_to` for deterministic time-dependent assertions:
```ruby
test "card expires after 30 days" do
card = cards(:one)
travel_to 31.days.from_now do
assert card.expired?
end
end
```
### External API Testing with VCR
Record and replay HTTP interactions:
```ruby
test "fetches user data from API" do
VCR.use_cassette("user_api") do
user_data = ExternalApi.fetch_user(123)
assert_equal "John", user_data[:name]
end
end
```
### Background Job Testing
Assert job enqueueing and email delivery:
```ruby
test "closing card enqueues notification job" do
card = cards(:one)
assert_enqueued_with(job: NotifyWatchersJob, args: [card]) do
card.close
end
end
test "welcome email is sent on signup" do
assert_emails 1 do
Identity.create!(email: "new@example.com")
end
end
```
### Testing Turbo Streams
```ruby
test "message creation broadcasts to room" do
room = rooms(:watercooler)
assert_turbo_stream_broadcasts [room, :messages] do
room.messages.create!(body: "Test", creator: users(:david))
end
end
```
## Testing Principles
### 1. Test Observable Behavior
Focus on what the code does, not how it does it:
```ruby
# ❌ Testing implementation
test "calls notify method on each watcher" do
card.expects(:notify).times(3)
card.close
end
# ✅ Testing behavior
test "watchers receive notifications when card closes" do
assert_difference -> { Notification.count }, 3 do
card.close
end
end
```
### 2. Don't Mock Everything
```ruby
# ❌ Over-mocked test
test "sending message" do
room = mock("room")
user = mock("user")
message = mock("message")
room.expects(:messages).returns(stub(create!: message))
message.expects(:broadcast_create)
MessagesController.new.create
end
# ✅ Test the real thing
test "sending message" do
sign_in users(:david)
post room_messages_url(rooms(:watercooler)),
params: { message: { body: "Hello" } }
assert_response :success
assert Message.exists?(body: "Hello")
end
```
### 3. Tests Ship with Features
Same commit, not TDD-first but together. Neither before (strict TDD) nor after (deferred testing).
### 4. Security Fixes Always Include Regression Tests
Every security fix must include a test that would have caught the vulnerability.
### 5. Integration Tests Validate Complete Workflows
Don't just test individual pieces - test that they work together.
## File Organization
```
test/
├── controllers/ # Integration tests for controllers
├── fixtures/ # YAML fixtures for all models
├── helpers/ # Helper method tests
├── integration/ # API integration tests
├── jobs/ # Background job tests
├── mailers/ # Mailer tests
├── models/ # Unit tests for models
├── system/ # Browser-based system tests
└── test_helper.rb # Test configuration
```
## Test Helper Setup
```ruby
# test/test_helper.rb
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
class ActiveSupport::TestCase
fixtures :all
parallelize(workers: :number_of_processors)
end
class ActionDispatch::IntegrationTest
include SignInHelper
end
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :selenium, using: :headless_chrome
end
```
## Sign In Helper
```ruby
# test/support/sign_in_helper.rb
module SignInHelper
def sign_in(user)
session = user.identity.sessions.create!
cookies.signed[:session_id] = session.id
end
end
```

View File

@@ -0,0 +1,627 @@
---
name: dspy-python
description: This skill should be used when working with DSPy, the Python framework for programming language models instead of prompting them. Use this when implementing LLM-powered features, creating DSPy signatures and modules, configuring language model providers (OpenAI, Anthropic, Gemini, Ollama), building agent systems with tools, optimizing prompts with teleprompters, integrating with FastAPI endpoints, or testing DSPy modules with pytest.
---
# DSPy Expert (Python)
## Overview
DSPy is a Python framework that enables developers to **program language models, not prompt them**. Instead of manually crafting prompts, define application requirements through composable, optimizable modules that can be tested, improved, and version-controlled like regular code.
This skill provides comprehensive guidance on:
- Creating signatures for LLM operations
- Building composable modules and workflows
- Configuring multiple LLM providers
- Implementing agents with tools (ReAct)
- Testing with pytest
- Optimizing with teleprompters (MIPROv2, BootstrapFewShot)
- Integrating with FastAPI for production APIs
- Production deployment patterns
## Core Capabilities
### 1. Signatures
Create input/output specifications for LLM operations using inline or class-based signatures.
**When to use**: Defining any LLM task, from simple classification to complex analysis.
**Quick reference**:
```python
import dspy
# Inline signature (simple tasks)
classify = dspy.Predict("email: str -> category: str, priority: str")
# Class-based signature (complex tasks with documentation)
class EmailClassification(dspy.Signature):
"""Classify customer support emails into categories."""
email_subject: str = dspy.InputField(desc="Subject line of the email")
email_body: str = dspy.InputField(desc="Full body content of the email")
category: str = dspy.OutputField(desc="One of: Technical, Billing, General")
priority: str = dspy.OutputField(desc="One of: Low, Medium, High")
```
**Templates**: See [signature-template.py](./assets/signature-template.py) for comprehensive examples including:
- Inline signatures for quick tasks
- Class-based signatures with type hints
- Signatures with Pydantic model outputs
- Multi-field complex signatures
**Best practices**:
- Always provide clear docstrings for class-based signatures
- Use `desc` parameter for field documentation
- Prefer specific descriptions over generic ones
- Use Pydantic models for structured complex outputs
**Full documentation**: See [core-concepts.md](./references/core-concepts.md) sections on Signatures and Type Safety.
### 2. Modules
Build reusable, composable modules that encapsulate LLM operations.
**When to use**: Implementing any LLM-powered feature, especially complex multi-step workflows.
**Quick reference**:
```python
import dspy
class EmailProcessor(dspy.Module):
def __init__(self):
super().__init__()
self.classifier = dspy.ChainOfThought(EmailClassification)
def forward(self, email_subject: str, email_body: str) -> dspy.Prediction:
return self.classifier(
email_subject=email_subject,
email_body=email_body
)
```
**Templates**: See [module-template.py](./assets/module-template.py) for comprehensive examples including:
- Basic modules with single predictors
- Multi-step pipelines that chain modules
- Modules with conditional logic
- Error handling and retry patterns
- Async modules for FastAPI
- Caching implementations
**Module composition**: Chain modules together to create complex workflows:
```python
class Pipeline(dspy.Module):
def __init__(self):
super().__init__()
self.step1 = Classifier()
self.step2 = Analyzer()
self.step3 = Responder()
def forward(self, input_text):
result1 = self.step1(text=input_text)
result2 = self.step2(classification=result1.category)
return self.step3(analysis=result2.analysis)
```
**Full documentation**: See [core-concepts.md](./references/core-concepts.md) sections on Modules and Module Composition.
### 3. Predictor Types
Choose the right predictor for your task:
**Predict**: Basic LLM inference
```python
predictor = dspy.Predict(TaskSignature)
result = predictor(input="data")
```
**ChainOfThought**: Adds automatic step-by-step reasoning
```python
predictor = dspy.ChainOfThought(TaskSignature)
result = predictor(input="data")
# result.reasoning contains the thought process
```
**ReAct**: Tool-using agents with iterative reasoning
```python
predictor = dspy.ReAct(
TaskSignature,
tools=[search_tool, calculator_tool],
max_iters=5
)
```
**ProgramOfThought**: Generates and executes Python code
```python
predictor = dspy.ProgramOfThought(TaskSignature)
result = predictor(task="Calculate factorial of 10")
```
**When to use each**:
- **Predict**: Simple tasks, classification, extraction
- **ChainOfThought**: Complex reasoning, analysis, multi-step thinking
- **ReAct**: Tasks requiring external tools (search, calculation, API calls)
- **ProgramOfThought**: Tasks best solved with generated code
**Full documentation**: See [core-concepts.md](./references/core-concepts.md) section on Predictors.
### 4. LLM Provider Configuration
Support for OpenAI, Anthropic Claude, Google, Ollama, and many more via LiteLLM.
**Quick configuration examples**:
```python
import dspy
# OpenAI
lm = dspy.LM('openai/gpt-4o-mini', api_key=os.environ['OPENAI_API_KEY'])
dspy.configure(lm=lm)
# Anthropic Claude
lm = dspy.LM('anthropic/claude-3-5-sonnet-20241022', api_key=os.environ['ANTHROPIC_API_KEY'])
dspy.configure(lm=lm)
# Google Gemini
lm = dspy.LM('google/gemini-1.5-pro', api_key=os.environ['GOOGLE_API_KEY'])
dspy.configure(lm=lm)
# Local Ollama (free, private)
lm = dspy.LM('ollama_chat/llama3.1', api_base='http://localhost:11434')
dspy.configure(lm=lm)
```
**Templates**: See [config-template.py](./assets/config-template.py) for comprehensive examples including:
- Environment-based configuration
- Multi-model setups for different tasks
- Async LM configuration
- Retry logic and fallback strategies
- Caching with dspy.cache
**Provider compatibility matrix**:
| Feature | OpenAI | Anthropic | Google | Ollama |
|---------|--------|-----------|--------|--------|
| Structured Output | Full | Full | Full | Partial |
| Vision (Images) | Full | Full | Full | Limited |
| Tool Calling | Full | Full | Full | Varies |
| Streaming | Full | Full | Full | Full |
**Cost optimization strategy**:
- Development: Ollama (free) or gpt-4o-mini (cheap)
- Testing: gpt-4o-mini with temperature=0.0
- Production simple tasks: gpt-4o-mini, claude-3-haiku, gemini-1.5-flash
- Production complex tasks: gpt-4o, claude-3-5-sonnet, gemini-1.5-pro
**Full documentation**: See [providers.md](./references/providers.md) for all configuration options.
### 5. FastAPI Integration
Serve DSPy modules as production API endpoints.
**Quick reference**:
```python
from fastapi import FastAPI
from pydantic import BaseModel
import dspy
app = FastAPI()
# Initialize DSPy
lm = dspy.LM('openai/gpt-4o-mini')
dspy.configure(lm=lm)
# Load optimized module
classifier = EmailProcessor()
class EmailRequest(BaseModel):
subject: str
body: str
class EmailResponse(BaseModel):
category: str
priority: str
@app.post("/classify", response_model=EmailResponse)
async def classify_email(request: EmailRequest):
result = classifier(
email_subject=request.subject,
email_body=request.body
)
return EmailResponse(
category=result.category,
priority=result.priority
)
```
**Production patterns**:
- Load optimized modules at startup
- Use Pydantic models for request/response validation
- Implement proper error handling
- Add observability with OpenTelemetry
- Use async where possible
**Full documentation**: See [fastapi-integration.md](./references/fastapi-integration.md) for complete patterns.
### 6. Testing DSPy Modules
Write standard pytest tests for LLM logic.
**Quick reference**:
```python
import pytest
import dspy
@pytest.fixture(scope="module")
def configure_dspy():
lm = dspy.LM('openai/gpt-4o-mini', api_key=os.environ['OPENAI_API_KEY'])
dspy.configure(lm=lm)
def test_email_classifier(configure_dspy):
classifier = EmailProcessor()
result = classifier(
email_subject="Can't log in",
email_body="Unable to access account"
)
assert result.category in ['Technical', 'Billing', 'General']
assert result.priority in ['High', 'Medium', 'Low']
def test_technical_email_classification(configure_dspy):
classifier = EmailProcessor()
result = classifier(
email_subject="Error 500 on checkout",
email_body="Getting server error when trying to complete purchase"
)
assert result.category == 'Technical'
```
**Testing patterns**:
- Use pytest fixtures for DSPy configuration
- Test type correctness of outputs
- Test edge cases (empty inputs, special characters, long texts)
- Use VCR/responses for deterministic API testing
- Integration test complete workflows
**Full documentation**: See [optimization.md](./references/optimization.md) section on Testing.
### 7. Optimization with Teleprompters
Automatically improve prompts and modules using optimization techniques.
**MIPROv2 optimization**:
```python
import dspy
from dspy.teleprompt import MIPROv2
# Define evaluation metric
def accuracy_metric(example, pred, trace=None):
return example.category == pred.category
# Prepare training data
trainset = [
dspy.Example(
email_subject="Can't log in",
email_body="Password reset not working",
category="Technical"
).with_inputs("email_subject", "email_body"),
# More examples...
]
# Run optimization
optimizer = MIPROv2(
metric=accuracy_metric,
num_candidates=10,
init_temperature=0.7
)
optimized_module = optimizer.compile(
EmailProcessor(),
trainset=trainset,
max_bootstrapped_demos=3,
max_labeled_demos=5
)
# Save optimized module
optimized_module.save("optimized_classifier.json")
```
**BootstrapFewShot** (simpler, faster):
```python
from dspy.teleprompt import BootstrapFewShot
optimizer = BootstrapFewShot(
metric=accuracy_metric,
max_bootstrapped_demos=4
)
optimized = optimizer.compile(
EmailProcessor(),
trainset=trainset
)
```
**Full documentation**: See [optimization.md](./references/optimization.md) section on Teleprompters.
### 8. Caching and Performance
Optimize performance with built-in caching.
**Enable caching**:
```python
import dspy
# Enable global caching
dspy.configure(
lm=lm,
cache=True # Uses SQLite by default
)
# Or with custom cache directory
dspy.configure(
lm=lm,
cache_dir="/path/to/cache"
)
```
**Cache control**:
```python
# Clear cache
dspy.cache.clear()
# Disable cache for specific call
with dspy.settings.context(cache=False):
result = module(input="data")
```
**Full documentation**: See [optimization.md](./references/optimization.md) section on Caching.
## Quick Start Workflow
### For New Projects
1. **Install DSPy**:
```bash
pip install dspy-ai
```
2. **Configure LLM provider** (see [config-template.py](./assets/config-template.py)):
```python
import dspy
import os
lm = dspy.LM('openai/gpt-4o-mini', api_key=os.environ['OPENAI_API_KEY'])
dspy.configure(lm=lm)
```
3. **Create a signature** (see [signature-template.py](./assets/signature-template.py)):
```python
class MySignature(dspy.Signature):
"""Clear description of task."""
input_field: str = dspy.InputField(desc="Description")
output_field: str = dspy.OutputField(desc="Description")
```
4. **Create a module** (see [module-template.py](./assets/module-template.py)):
```python
class MyModule(dspy.Module):
def __init__(self):
super().__init__()
self.predictor = dspy.Predict(MySignature)
def forward(self, input_field: str):
return self.predictor(input_field=input_field)
```
5. **Use the module**:
```python
module = MyModule()
result = module(input_field="test")
print(result.output_field)
```
6. **Add tests** (see [optimization.md](./references/optimization.md)):
```python
def test_my_module():
result = MyModule()(input_field="test")
assert isinstance(result.output_field, str)
```
### For FastAPI Applications
1. **Install dependencies**:
```bash
pip install dspy-ai fastapi uvicorn pydantic
```
2. **Create app structure**:
```
my_app/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI app
│ ├── dspy_modules/ # DSPy modules
│ │ ├── __init__.py
│ │ └── classifier.py
│ ├── models/ # Pydantic models
│ │ └── __init__.py
│ └── config.py # DSPy configuration
├── tests/
│ └── test_classifier.py
└── requirements.txt
```
3. **Configure DSPy** in `config.py`:
```python
import dspy
import os
def configure_dspy():
lm = dspy.LM(
'openai/gpt-4o-mini',
api_key=os.environ['OPENAI_API_KEY']
)
dspy.configure(lm=lm, cache=True)
```
4. **Create FastAPI app** in `main.py`:
```python
from fastapi import FastAPI
from contextlib import asynccontextmanager
from app.config import configure_dspy
from app.dspy_modules.classifier import EmailProcessor
@asynccontextmanager
async def lifespan(app: FastAPI):
configure_dspy()
yield
app = FastAPI(lifespan=lifespan)
classifier = EmailProcessor()
@app.post("/classify")
async def classify(request: EmailRequest):
result = classifier(
email_subject=request.subject,
email_body=request.body
)
return {"category": result.category, "priority": result.priority}
```
## Common Patterns
### Pattern: Multi-Step Analysis Pipeline
```python
class AnalysisPipeline(dspy.Module):
def __init__(self):
super().__init__()
self.extract = dspy.Predict(ExtractSignature)
self.analyze = dspy.ChainOfThought(AnalyzeSignature)
self.summarize = dspy.Predict(SummarizeSignature)
def forward(self, text: str):
extracted = self.extract(text=text)
analyzed = self.analyze(data=extracted.data)
return self.summarize(analysis=analyzed.result)
```
### Pattern: Agent with Tools
```python
import dspy
def search_web(query: str) -> str:
"""Search the web for information."""
# Implementation here
return f"Results for: {query}"
def calculate(expression: str) -> str:
"""Evaluate a mathematical expression."""
return str(eval(expression))
class ResearchAgent(dspy.Module):
def __init__(self):
super().__init__()
self.agent = dspy.ReAct(
ResearchSignature,
tools=[search_web, calculate],
max_iters=10
)
def forward(self, question: str):
return self.agent(question=question)
```
### Pattern: Conditional Routing
```python
class SmartRouter(dspy.Module):
def __init__(self):
super().__init__()
self.classifier = dspy.Predict(ClassifyComplexity)
self.simple_handler = SimpleModule()
self.complex_handler = ComplexModule()
def forward(self, input_text: str):
classification = self.classifier(text=input_text)
if classification.complexity == "Simple":
return self.simple_handler(input=input_text)
else:
return self.complex_handler(input=input_text)
```
### Pattern: Retry with Fallback
```python
import dspy
from tenacity import retry, stop_after_attempt, wait_exponential
class RobustModule(dspy.Module):
def __init__(self):
super().__init__()
self.predictor = dspy.Predict(TaskSignature)
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=2, max=10)
)
def forward(self, input_text: str):
result = self.predictor(input=input_text)
self._validate(result)
return result
def _validate(self, result):
if not result.output:
raise ValueError("Empty output from LLM")
```
### Pattern: Pydantic Output Models
```python
from pydantic import BaseModel, Field
import dspy
class ClassificationResult(BaseModel):
category: str = Field(description="Category: Technical, Billing, or General")
priority: str = Field(description="Priority: Low, Medium, or High")
confidence: float = Field(ge=0.0, le=1.0, description="Confidence score")
class TypedClassifier(dspy.Signature):
"""Classify with structured output."""
text: str = dspy.InputField()
result: ClassificationResult = dspy.OutputField()
```
## Resources
This skill includes comprehensive reference materials and templates:
### References (load as needed for detailed information)
- [core-concepts.md](./references/core-concepts.md): Complete guide to signatures, modules, predictors, and best practices
- [providers.md](./references/providers.md): All LLM provider configurations, compatibility matrix, and troubleshooting
- [optimization.md](./references/optimization.md): Testing patterns, teleprompters, caching, and monitoring
- [fastapi-integration.md](./references/fastapi-integration.md): Production patterns for serving DSPy with FastAPI
### Assets (templates for quick starts)
- [signature-template.py](./assets/signature-template.py): Examples of signatures including inline, class-based, and Pydantic outputs
- [module-template.py](./assets/module-template.py): Module patterns including pipelines, agents, async, and caching
- [config-template.py](./assets/config-template.py): Configuration examples for all providers and environments
## When to Use This Skill
Trigger this skill when:
- Implementing LLM-powered features in Python applications
- Creating programmatic interfaces for AI operations
- Building agent systems with tool usage
- Setting up or troubleshooting LLM providers with DSPy
- Optimizing prompts using teleprompters
- Testing LLM functionality with pytest
- Integrating DSPy with FastAPI
- Converting from manual prompt engineering to programmatic approach
- Debugging DSPy code or configuration issues

View File

@@ -1,594 +0,0 @@
---
name: dspy-ruby
description: This skill should be used when working with DSPy.rb, a Ruby framework for building type-safe, composable LLM applications. Use this when implementing predictable AI features, creating LLM signatures and modules, configuring language model providers (OpenAI, Anthropic, Gemini, Ollama), building agent systems with tools, optimizing prompts, or testing LLM-powered functionality in Ruby applications.
---
# DSPy.rb Expert
## Overview
DSPy.rb is a Ruby framework that enables developers to **program LLMs, not prompt them**. Instead of manually crafting prompts, define application requirements through type-safe, composable modules that can be tested, optimized, and version-controlled like regular code.
This skill provides comprehensive guidance on:
- Creating type-safe signatures for LLM operations
- Building composable modules and workflows
- Configuring multiple LLM providers
- Implementing agents with tools
- Testing and optimizing LLM applications
- Production deployment patterns
## Core Capabilities
### 1. Type-Safe Signatures
Create input/output contracts for LLM operations with runtime type checking.
**When to use**: Defining any LLM task, from simple classification to complex analysis.
**Quick reference**:
```ruby
class EmailClassificationSignature < DSPy::Signature
description "Classify customer support emails"
input do
const :email_subject, String
const :email_body, String
end
output do
const :category, T.enum(["Technical", "Billing", "General"])
const :priority, T.enum(["Low", "Medium", "High"])
end
end
```
**Templates**: See `assets/signature-template.rb` for comprehensive examples including:
- Basic signatures with multiple field types
- Vision signatures for multimodal tasks
- Sentiment analysis signatures
- Code generation signatures
**Best practices**:
- Always provide clear, specific descriptions
- Use enums for constrained outputs
- Include field descriptions with `desc:` parameter
- Prefer specific types over generic String when possible
**Full documentation**: See `references/core-concepts.md` sections on Signatures and Type Safety.
### 2. Composable Modules
Build reusable, chainable modules that encapsulate LLM operations.
**When to use**: Implementing any LLM-powered feature, especially complex multi-step workflows.
**Quick reference**:
```ruby
class EmailProcessor < DSPy::Module
def initialize
super
@classifier = DSPy::Predict.new(EmailClassificationSignature)
end
def forward(email_subject:, email_body:)
@classifier.forward(
email_subject: email_subject,
email_body: email_body
)
end
end
```
**Templates**: See `assets/module-template.rb` for comprehensive examples including:
- Basic modules with single predictors
- Multi-step pipelines that chain modules
- Modules with conditional logic
- Error handling and retry patterns
- Stateful modules with history
- Caching implementations
**Module composition**: Chain modules together to create complex workflows:
```ruby
class Pipeline < DSPy::Module
def initialize
super
@step1 = Classifier.new
@step2 = Analyzer.new
@step3 = Responder.new
end
def forward(input)
result1 = @step1.forward(input)
result2 = @step2.forward(result1)
@step3.forward(result2)
end
end
```
**Full documentation**: See `references/core-concepts.md` sections on Modules and Module Composition.
### 3. Multiple Predictor Types
Choose the right predictor for your task:
**Predict**: Basic LLM inference with type-safe inputs/outputs
```ruby
predictor = DSPy::Predict.new(TaskSignature)
result = predictor.forward(input: "data")
```
**ChainOfThought**: Adds automatic reasoning for improved accuracy
```ruby
predictor = DSPy::ChainOfThought.new(TaskSignature)
result = predictor.forward(input: "data")
# Returns: { reasoning: "...", output: "..." }
```
**ReAct**: Tool-using agents with iterative reasoning
```ruby
predictor = DSPy::ReAct.new(
TaskSignature,
tools: [SearchTool.new, CalculatorTool.new],
max_iterations: 5
)
```
**CodeAct**: Dynamic code generation (requires `dspy-code_act` gem)
```ruby
predictor = DSPy::CodeAct.new(TaskSignature)
result = predictor.forward(task: "Calculate factorial of 5")
```
**When to use each**:
- **Predict**: Simple tasks, classification, extraction
- **ChainOfThought**: Complex reasoning, analysis, multi-step thinking
- **ReAct**: Tasks requiring external tools (search, calculation, API calls)
- **CodeAct**: Tasks best solved with generated code
**Full documentation**: See `references/core-concepts.md` section on Predictors.
### 4. LLM Provider Configuration
Support for OpenAI, Anthropic Claude, Google Gemini, Ollama, and OpenRouter.
**Quick configuration examples**:
```ruby
# OpenAI
DSPy.configure do |c|
c.lm = DSPy::LM.new('openai/gpt-4o-mini',
api_key: ENV['OPENAI_API_KEY'])
end
# Anthropic Claude
DSPy.configure do |c|
c.lm = DSPy::LM.new('anthropic/claude-3-5-sonnet-20241022',
api_key: ENV['ANTHROPIC_API_KEY'])
end
# Google Gemini
DSPy.configure do |c|
c.lm = DSPy::LM.new('gemini/gemini-1.5-pro',
api_key: ENV['GOOGLE_API_KEY'])
end
# Local Ollama (free, private)
DSPy.configure do |c|
c.lm = DSPy::LM.new('ollama/llama3.1')
end
```
**Templates**: See `assets/config-template.rb` for comprehensive examples including:
- Environment-based configuration
- Multi-model setups for different tasks
- Configuration with observability (OpenTelemetry, Langfuse)
- Retry logic and fallback strategies
- Budget tracking
- Rails initializer patterns
**Provider compatibility matrix**:
| Feature | OpenAI | Anthropic | Gemini | Ollama |
|---------|--------|-----------|--------|--------|
| Structured Output | ✅ | ✅ | ✅ | ✅ |
| Vision (Images) | ✅ | ✅ | ✅ | ⚠️ Limited |
| Image URLs | ✅ | ❌ | ❌ | ❌ |
| Tool Calling | ✅ | ✅ | ✅ | Varies |
**Cost optimization strategy**:
- Development: Ollama (free) or gpt-4o-mini (cheap)
- Testing: gpt-4o-mini with temperature=0.0
- Production simple tasks: gpt-4o-mini, claude-3-haiku, gemini-1.5-flash
- Production complex tasks: gpt-4o, claude-3-5-sonnet, gemini-1.5-pro
**Full documentation**: See `references/providers.md` for all configuration options, provider-specific features, and troubleshooting.
### 5. Multimodal & Vision Support
Process images alongside text using the unified `DSPy::Image` interface.
**Quick reference**:
```ruby
class VisionSignature < DSPy::Signature
description "Analyze image and answer questions"
input do
const :image, DSPy::Image
const :question, String
end
output do
const :answer, String
end
end
predictor = DSPy::Predict.new(VisionSignature)
result = predictor.forward(
image: DSPy::Image.from_file("path/to/image.jpg"),
question: "What objects are visible?"
)
```
**Image loading methods**:
```ruby
# From file
DSPy::Image.from_file("path/to/image.jpg")
# From URL (OpenAI only)
DSPy::Image.from_url("https://example.com/image.jpg")
# From base64
DSPy::Image.from_base64(base64_data, mime_type: "image/jpeg")
```
**Provider support**:
- OpenAI: Full support including URLs
- Anthropic, Gemini: Base64 or file loading only
- Ollama: Limited multimodal depending on model
**Full documentation**: See `references/core-concepts.md` section on Multimodal Support.
### 6. Testing LLM Applications
Write standard RSpec tests for LLM logic.
**Quick reference**:
```ruby
RSpec.describe EmailClassifier do
before do
DSPy.configure do |c|
c.lm = DSPy::LM.new('openai/gpt-4o-mini',
api_key: ENV['OPENAI_API_KEY'])
end
end
it 'classifies technical emails correctly' do
classifier = EmailClassifier.new
result = classifier.forward(
email_subject: "Can't log in",
email_body: "Unable to access account"
)
expect(result[:category]).to eq('Technical')
expect(result[:priority]).to be_in(['High', 'Medium', 'Low'])
end
end
```
**Testing patterns**:
- Mock LLM responses for unit tests
- Use VCR for deterministic API testing
- Test type safety and validation
- Test edge cases (empty inputs, special characters, long texts)
- Integration test complete workflows
**Full documentation**: See `references/optimization.md` section on Testing.
### 7. Optimization & Improvement
Automatically improve prompts and modules using optimization techniques.
**MIPROv2 optimization**:
```ruby
require 'dspy/mipro'
# Define evaluation metric
def accuracy_metric(example, prediction)
example[:expected_output][:category] == prediction[:category] ? 1.0 : 0.0
end
# Prepare training data
training_examples = [
{
input: { email_subject: "...", email_body: "..." },
expected_output: { category: 'Technical' }
},
# More examples...
]
# Run optimization
optimizer = DSPy::MIPROv2.new(
metric: method(:accuracy_metric),
num_candidates: 10
)
optimized_module = optimizer.compile(
EmailClassifier.new,
trainset: training_examples
)
```
**A/B testing different approaches**:
```ruby
# Test ChainOfThought vs ReAct
approach_a_score = evaluate_approach(ChainOfThoughtModule, test_set)
approach_b_score = evaluate_approach(ReActModule, test_set)
```
**Full documentation**: See `references/optimization.md` section on Optimization.
### 8. Observability & Monitoring
Track performance, token usage, and behavior in production.
**OpenTelemetry integration**:
```ruby
require 'opentelemetry/sdk'
OpenTelemetry::SDK.configure do |c|
c.service_name = 'my-dspy-app'
c.use_all
end
# DSPy automatically creates traces
```
**Langfuse tracing**:
```ruby
DSPy.configure do |c|
c.lm = DSPy::LM.new('openai/gpt-4o-mini',
api_key: ENV['OPENAI_API_KEY'])
c.langfuse = {
public_key: ENV['LANGFUSE_PUBLIC_KEY'],
secret_key: ENV['LANGFUSE_SECRET_KEY']
}
end
```
**Custom monitoring**:
- Token tracking
- Performance monitoring
- Error rate tracking
- Custom logging
**Full documentation**: See `references/optimization.md` section on Observability.
## Quick Start Workflow
### For New Projects
1. **Install DSPy.rb and provider gems**:
```bash
gem install dspy dspy-openai # or dspy-anthropic, dspy-gemini
```
2. **Configure LLM provider** (see `assets/config-template.rb`):
```ruby
require 'dspy'
DSPy.configure do |c|
c.lm = DSPy::LM.new('openai/gpt-4o-mini',
api_key: ENV['OPENAI_API_KEY'])
end
```
3. **Create a signature** (see `assets/signature-template.rb`):
```ruby
class MySignature < DSPy::Signature
description "Clear description of task"
input do
const :input_field, String, desc: "Description"
end
output do
const :output_field, String, desc: "Description"
end
end
```
4. **Create a module** (see `assets/module-template.rb`):
```ruby
class MyModule < DSPy::Module
def initialize
super
@predictor = DSPy::Predict.new(MySignature)
end
def forward(input_field:)
@predictor.forward(input_field: input_field)
end
end
```
5. **Use the module**:
```ruby
module_instance = MyModule.new
result = module_instance.forward(input_field: "test")
puts result[:output_field]
```
6. **Add tests** (see `references/optimization.md`):
```ruby
RSpec.describe MyModule do
it 'produces expected output' do
result = MyModule.new.forward(input_field: "test")
expect(result[:output_field]).to be_a(String)
end
end
```
### For Rails Applications
1. **Add to Gemfile**:
```ruby
gem 'dspy'
gem 'dspy-openai' # or other provider
```
2. **Create initializer** at `config/initializers/dspy.rb` (see `assets/config-template.rb` for full example):
```ruby
require 'dspy'
DSPy.configure do |c|
c.lm = DSPy::LM.new('openai/gpt-4o-mini',
api_key: ENV['OPENAI_API_KEY'])
end
```
3. **Create modules in** `app/llm/` directory:
```ruby
# app/llm/email_classifier.rb
class EmailClassifier < DSPy::Module
# Implementation here
end
```
4. **Use in controllers/services**:
```ruby
class EmailsController < ApplicationController
def classify
classifier = EmailClassifier.new
result = classifier.forward(
email_subject: params[:subject],
email_body: params[:body]
)
render json: result
end
end
```
## Common Patterns
### Pattern: Multi-Step Analysis Pipeline
```ruby
class AnalysisPipeline < DSPy::Module
def initialize
super
@extract = DSPy::Predict.new(ExtractSignature)
@analyze = DSPy::ChainOfThought.new(AnalyzeSignature)
@summarize = DSPy::Predict.new(SummarizeSignature)
end
def forward(text:)
extracted = @extract.forward(text: text)
analyzed = @analyze.forward(data: extracted[:data])
@summarize.forward(analysis: analyzed[:result])
end
end
```
### Pattern: Agent with Tools
```ruby
class ResearchAgent < DSPy::Module
def initialize
super
@agent = DSPy::ReAct.new(
ResearchSignature,
tools: [
WebSearchTool.new,
DatabaseQueryTool.new,
SummarizerTool.new
],
max_iterations: 10
)
end
def forward(question:)
@agent.forward(question: question)
end
end
class WebSearchTool < DSPy::Tool
def call(query:)
results = perform_search(query)
{ results: results }
end
end
```
### Pattern: Conditional Routing
```ruby
class SmartRouter < DSPy::Module
def initialize
super
@classifier = DSPy::Predict.new(ClassifySignature)
@simple_handler = SimpleModule.new
@complex_handler = ComplexModule.new
end
def forward(input:)
classification = @classifier.forward(text: input)
if classification[:complexity] == 'Simple'
@simple_handler.forward(input: input)
else
@complex_handler.forward(input: input)
end
end
end
```
### Pattern: Retry with Fallback
```ruby
class RobustModule < DSPy::Module
MAX_RETRIES = 3
def forward(input, retry_count: 0)
begin
@predictor.forward(input)
rescue DSPy::ValidationError => e
if retry_count < MAX_RETRIES
sleep(2 ** retry_count)
forward(input, retry_count: retry_count + 1)
else
# Fallback to default or raise
raise
end
end
end
end
```
## Resources
This skill includes comprehensive reference materials and templates:
### References (load as needed for detailed information)
- [core-concepts.md](./references/core-concepts.md): Complete guide to signatures, modules, predictors, multimodal support, and best practices
- [providers.md](./references/providers.md): All LLM provider configurations, compatibility matrix, cost optimization, and troubleshooting
- [optimization.md](./references/optimization.md): Testing patterns, optimization techniques, observability setup, and monitoring
### Assets (templates for quick starts)
- [signature-template.rb](./assets/signature-template.rb): Examples of signatures including basic, vision, sentiment analysis, and code generation
- [module-template.rb](./assets/module-template.rb): Module patterns including pipelines, agents, error handling, caching, and state management
- [config-template.rb](./assets/config-template.rb): Configuration examples for all providers, environments, observability, and production patterns
## When to Use This Skill
Trigger this skill when:
- Implementing LLM-powered features in Ruby applications
- Creating type-safe interfaces for AI operations
- Building agent systems with tool usage
- Setting up or troubleshooting LLM providers
- Optimizing prompts and improving accuracy
- Testing LLM functionality
- Adding observability to AI applications
- Converting from manual prompt engineering to programmatic approach
- Debugging DSPy.rb code or configuration issues

View File

@@ -1,359 +0,0 @@
# frozen_string_literal: true
# DSPy.rb Configuration Examples
# This file demonstrates various configuration patterns for different use cases
require 'dspy'
# ============================================================================
# Basic Configuration
# ============================================================================
# Simple OpenAI configuration
DSPy.configure do |c|
c.lm = DSPy::LM.new('openai/gpt-4o-mini',
api_key: ENV['OPENAI_API_KEY'])
end
# ============================================================================
# Multi-Provider Configuration
# ============================================================================
# Anthropic Claude
DSPy.configure do |c|
c.lm = DSPy::LM.new('anthropic/claude-3-5-sonnet-20241022',
api_key: ENV['ANTHROPIC_API_KEY'])
end
# Google Gemini
DSPy.configure do |c|
c.lm = DSPy::LM.new('gemini/gemini-1.5-pro',
api_key: ENV['GOOGLE_API_KEY'])
end
# Local Ollama
DSPy.configure do |c|
c.lm = DSPy::LM.new('ollama/llama3.1',
base_url: 'http://localhost:11434')
end
# OpenRouter (access to 200+ models)
DSPy.configure do |c|
c.lm = DSPy::LM.new('openrouter/anthropic/claude-3.5-sonnet',
api_key: ENV['OPENROUTER_API_KEY'],
base_url: 'https://openrouter.ai/api/v1')
end
# ============================================================================
# Environment-Based Configuration
# ============================================================================
# Different models for different environments
if Rails.env.development?
# Use local Ollama for development (free, private)
DSPy.configure do |c|
c.lm = DSPy::LM.new('ollama/llama3.1')
end
elsif Rails.env.test?
# Use cheap model for testing
DSPy.configure do |c|
c.lm = DSPy::LM.new('openai/gpt-4o-mini',
api_key: ENV['OPENAI_API_KEY'])
end
else
# Use powerful model for production
DSPy.configure do |c|
c.lm = DSPy::LM.new('anthropic/claude-3-5-sonnet-20241022',
api_key: ENV['ANTHROPIC_API_KEY'])
end
end
# ============================================================================
# Configuration with Custom Parameters
# ============================================================================
DSPy.configure do |c|
c.lm = DSPy::LM.new('openai/gpt-4o',
api_key: ENV['OPENAI_API_KEY'],
temperature: 0.7, # Creativity (0.0-2.0, default: 1.0)
max_tokens: 2000, # Maximum response length
top_p: 0.9, # Nucleus sampling
frequency_penalty: 0.0, # Reduce repetition (-2.0 to 2.0)
presence_penalty: 0.0 # Encourage new topics (-2.0 to 2.0)
)
end
# ============================================================================
# Multiple Model Configuration (Task-Specific)
# ============================================================================
# Create different language models for different tasks
module MyApp
# Fast model for simple tasks
FAST_LM = DSPy::LM.new('openai/gpt-4o-mini',
api_key: ENV['OPENAI_API_KEY'],
temperature: 0.3 # More deterministic
)
# Powerful model for complex tasks
POWERFUL_LM = DSPy::LM.new('anthropic/claude-3-5-sonnet-20241022',
api_key: ENV['ANTHROPIC_API_KEY'],
temperature: 0.7
)
# Creative model for content generation
CREATIVE_LM = DSPy::LM.new('openai/gpt-4o',
api_key: ENV['OPENAI_API_KEY'],
temperature: 1.2, # More creative
top_p: 0.95
)
# Vision-capable model
VISION_LM = DSPy::LM.new('openai/gpt-4o',
api_key: ENV['OPENAI_API_KEY'])
end
# Use in modules
class SimpleClassifier < DSPy::Module
def initialize
super
DSPy.configure { |c| c.lm = MyApp::FAST_LM }
@predictor = DSPy::Predict.new(SimpleSignature)
end
end
class ComplexAnalyzer < DSPy::Module
def initialize
super
DSPy.configure { |c| c.lm = MyApp::POWERFUL_LM }
@predictor = DSPy::ChainOfThought.new(ComplexSignature)
end
end
# ============================================================================
# Configuration with Observability (OpenTelemetry)
# ============================================================================
require 'opentelemetry/sdk'
# Configure OpenTelemetry
OpenTelemetry::SDK.configure do |c|
c.service_name = 'my-dspy-app'
c.use_all
end
# Configure DSPy (automatically integrates with OpenTelemetry)
DSPy.configure do |c|
c.lm = DSPy::LM.new('openai/gpt-4o-mini',
api_key: ENV['OPENAI_API_KEY'])
end
# ============================================================================
# Configuration with Langfuse Tracing
# ============================================================================
require 'dspy/langfuse'
DSPy.configure do |c|
c.lm = DSPy::LM.new('openai/gpt-4o-mini',
api_key: ENV['OPENAI_API_KEY'])
# Enable Langfuse tracing
c.langfuse = {
public_key: ENV['LANGFUSE_PUBLIC_KEY'],
secret_key: ENV['LANGFUSE_SECRET_KEY'],
host: ENV['LANGFUSE_HOST'] || 'https://cloud.langfuse.com'
}
end
# ============================================================================
# Configuration with Retry Logic
# ============================================================================
class RetryableConfig
MAX_RETRIES = 3
def self.configure
DSPy.configure do |c|
c.lm = create_lm_with_retry
end
end
def self.create_lm_with_retry
lm = DSPy::LM.new('openai/gpt-4o-mini',
api_key: ENV['OPENAI_API_KEY'])
# Wrap with retry logic
lm.extend(RetryBehavior)
lm
end
module RetryBehavior
def forward(input, retry_count: 0)
super(input)
rescue RateLimitError, TimeoutError => e
if retry_count < MAX_RETRIES
sleep(2 ** retry_count) # Exponential backoff
forward(input, retry_count: retry_count + 1)
else
raise
end
end
end
end
RetryableConfig.configure
# ============================================================================
# Configuration with Fallback Models
# ============================================================================
class FallbackConfig
def self.configure
DSPy.configure do |c|
c.lm = create_lm_with_fallback
end
end
def self.create_lm_with_fallback
primary = DSPy::LM.new('anthropic/claude-3-5-sonnet-20241022',
api_key: ENV['ANTHROPIC_API_KEY'])
fallback = DSPy::LM.new('openai/gpt-4o',
api_key: ENV['OPENAI_API_KEY'])
FallbackLM.new(primary, fallback)
end
class FallbackLM
def initialize(primary, fallback)
@primary = primary
@fallback = fallback
end
def forward(input)
@primary.forward(input)
rescue => e
puts "Primary model failed: #{e.message}. Falling back..."
@fallback.forward(input)
end
end
end
FallbackConfig.configure
# ============================================================================
# Configuration with Budget Tracking
# ============================================================================
class BudgetTrackedConfig
def self.configure(monthly_budget_usd:)
DSPy.configure do |c|
c.lm = BudgetTracker.new(
DSPy::LM.new('openai/gpt-4o',
api_key: ENV['OPENAI_API_KEY']),
monthly_budget_usd: monthly_budget_usd
)
end
end
class BudgetTracker
def initialize(lm, monthly_budget_usd:)
@lm = lm
@monthly_budget_usd = monthly_budget_usd
@monthly_cost = 0.0
end
def forward(input)
result = @lm.forward(input)
# Track cost (simplified - actual costs vary by model)
tokens = result.metadata[:usage][:total_tokens]
cost = estimate_cost(tokens)
@monthly_cost += cost
if @monthly_cost > @monthly_budget_usd
raise "Monthly budget of $#{@monthly_budget_usd} exceeded!"
end
result
end
private
def estimate_cost(tokens)
# Simplified cost estimation (check provider pricing)
(tokens / 1_000_000.0) * 5.0 # $5 per 1M tokens
end
end
end
BudgetTrackedConfig.configure(monthly_budget_usd: 100)
# ============================================================================
# Configuration Initializer for Rails
# ============================================================================
# Save this as config/initializers/dspy.rb
#
# require 'dspy'
#
# DSPy.configure do |c|
# # Environment-specific configuration
# model_config = case Rails.env.to_sym
# when :development
# { provider: 'ollama', model: 'llama3.1' }
# when :test
# { provider: 'openai', model: 'gpt-4o-mini', temperature: 0.0 }
# when :production
# { provider: 'anthropic', model: 'claude-3-5-sonnet-20241022' }
# end
#
# # Configure language model
# c.lm = DSPy::LM.new(
# "#{model_config[:provider]}/#{model_config[:model]}",
# api_key: ENV["#{model_config[:provider].upcase}_API_KEY"],
# **model_config.except(:provider, :model)
# )
#
# # Optional: Add observability
# if Rails.env.production?
# c.langfuse = {
# public_key: ENV['LANGFUSE_PUBLIC_KEY'],
# secret_key: ENV['LANGFUSE_SECRET_KEY']
# }
# end
# end
# ============================================================================
# Testing Configuration
# ============================================================================
# In spec/spec_helper.rb or test/test_helper.rb
#
# RSpec.configure do |config|
# config.before(:suite) do
# DSPy.configure do |c|
# c.lm = DSPy::LM.new('openai/gpt-4o-mini',
# api_key: ENV['OPENAI_API_KEY'],
# temperature: 0.0 # Deterministic for testing
# )
# end
# end
# end
# ============================================================================
# Configuration Best Practices
# ============================================================================
# 1. Use environment variables for API keys (never hardcode)
# 2. Use different models for different environments
# 3. Use cheaper/faster models for development and testing
# 4. Configure temperature based on use case:
# - 0.0-0.3: Deterministic, factual tasks
# - 0.7-1.0: Balanced creativity
# - 1.0-2.0: High creativity, content generation
# 5. Add observability in production (OpenTelemetry, Langfuse)
# 6. Implement retry logic and fallbacks for reliability
# 7. Track costs and set budgets for production
# 8. Use max_tokens to control response length and costs

View File

@@ -1,326 +0,0 @@
# frozen_string_literal: true
# Example DSPy Module Template
# This template demonstrates best practices for creating composable modules
# Basic module with single predictor
class BasicModule < DSPy::Module
def initialize
super
# Initialize predictor with signature
@predictor = DSPy::Predict.new(ExampleSignature)
end
def forward(input_hash)
# Forward pass through the predictor
@predictor.forward(input_hash)
end
end
# Module with Chain of Thought reasoning
class ChainOfThoughtModule < DSPy::Module
def initialize
super
# ChainOfThought automatically adds reasoning to output
@predictor = DSPy::ChainOfThought.new(EmailClassificationSignature)
end
def forward(email_subject:, email_body:)
result = @predictor.forward(
email_subject: email_subject,
email_body: email_body
)
# Result includes :reasoning field automatically
{
category: result[:category],
priority: result[:priority],
reasoning: result[:reasoning],
confidence: calculate_confidence(result)
}
end
private
def calculate_confidence(result)
# Add custom logic to calculate confidence
# For example, based on reasoning length or specificity
result[:confidence] || 0.8
end
end
# Composable module that chains multiple steps
class MultiStepPipeline < DSPy::Module
def initialize
super
# Initialize multiple predictors for different steps
@step1 = DSPy::Predict.new(Step1Signature)
@step2 = DSPy::ChainOfThought.new(Step2Signature)
@step3 = DSPy::Predict.new(Step3Signature)
end
def forward(input)
# Chain predictors together
result1 = @step1.forward(input)
result2 = @step2.forward(result1)
result3 = @step3.forward(result2)
# Combine results as needed
{
step1_output: result1,
step2_output: result2,
final_result: result3
}
end
end
# Module with conditional logic
class ConditionalModule < DSPy::Module
def initialize
super
@simple_classifier = DSPy::Predict.new(SimpleClassificationSignature)
@complex_analyzer = DSPy::ChainOfThought.new(ComplexAnalysisSignature)
end
def forward(text:, complexity_threshold: 100)
# Use different predictors based on input characteristics
if text.length < complexity_threshold
@simple_classifier.forward(text: text)
else
@complex_analyzer.forward(text: text)
end
end
end
# Module with error handling and retry logic
class RobustModule < DSPy::Module
MAX_RETRIES = 3
def initialize
super
@predictor = DSPy::Predict.new(RobustSignature)
@logger = Logger.new(STDOUT)
end
def forward(input, retry_count: 0)
@logger.info "Processing input: #{input.inspect}"
begin
result = @predictor.forward(input)
validate_result!(result)
result
rescue DSPy::ValidationError => e
@logger.error "Validation error: #{e.message}"
if retry_count < MAX_RETRIES
@logger.info "Retrying (#{retry_count + 1}/#{MAX_RETRIES})..."
sleep(2 ** retry_count) # Exponential backoff
forward(input, retry_count: retry_count + 1)
else
@logger.error "Max retries exceeded"
raise
end
end
end
private
def validate_result!(result)
# Add custom validation logic
raise DSPy::ValidationError, "Invalid result" unless result[:category]
raise DSPy::ValidationError, "Low confidence" if result[:confidence] && result[:confidence] < 0.5
end
end
# Module with ReAct agent and tools
class AgentModule < DSPy::Module
def initialize
super
# Define tools for the agent
tools = [
SearchTool.new,
CalculatorTool.new,
DatabaseQueryTool.new
]
# ReAct provides iterative reasoning and tool usage
@agent = DSPy::ReAct.new(
AgentSignature,
tools: tools,
max_iterations: 5
)
end
def forward(task:)
# Agent will autonomously use tools to complete the task
@agent.forward(task: task)
end
end
# Tool definition example
class SearchTool < DSPy::Tool
def call(query:)
# Implement search functionality
results = perform_search(query)
{ results: results }
end
private
def perform_search(query)
# Actual search implementation
# Could call external API, database, etc.
["result1", "result2", "result3"]
end
end
# Module with state management
class StatefulModule < DSPy::Module
attr_reader :history
def initialize
super
@predictor = DSPy::ChainOfThought.new(StatefulSignature)
@history = []
end
def forward(input)
# Process with context from history
context = build_context_from_history
result = @predictor.forward(
input: input,
context: context
)
# Store in history
@history << {
input: input,
result: result,
timestamp: Time.now
}
result
end
def reset!
@history.clear
end
private
def build_context_from_history
@history.last(5).map { |h| h[:result][:summary] }.join("\n")
end
end
# Module that uses different LLMs for different tasks
class MultiModelModule < DSPy::Module
def initialize
super
# Fast, cheap model for simple classification
@fast_predictor = create_predictor(
'openai/gpt-4o-mini',
SimpleClassificationSignature
)
# Powerful model for complex analysis
@powerful_predictor = create_predictor(
'anthropic/claude-3-5-sonnet-20241022',
ComplexAnalysisSignature
)
end
def forward(input, use_complex: false)
if use_complex
@powerful_predictor.forward(input)
else
@fast_predictor.forward(input)
end
end
private
def create_predictor(model, signature)
lm = DSPy::LM.new(model, api_key: ENV["#{model.split('/').first.upcase}_API_KEY"])
DSPy::Predict.new(signature, lm: lm)
end
end
# Module with caching
class CachedModule < DSPy::Module
def initialize
super
@predictor = DSPy::Predict.new(CachedSignature)
@cache = {}
end
def forward(input)
# Create cache key from input
cache_key = create_cache_key(input)
# Return cached result if available
if @cache.key?(cache_key)
puts "Cache hit for #{cache_key}"
return @cache[cache_key]
end
# Compute and cache result
result = @predictor.forward(input)
@cache[cache_key] = result
result
end
def clear_cache!
@cache.clear
end
private
def create_cache_key(input)
# Create deterministic hash from input
Digest::MD5.hexdigest(input.to_s)
end
end
# Usage Examples:
#
# Basic usage:
# module = BasicModule.new
# result = module.forward(field_name: "value")
#
# Chain of Thought:
# module = ChainOfThoughtModule.new
# result = module.forward(
# email_subject: "Can't log in",
# email_body: "I'm unable to access my account"
# )
# puts result[:reasoning]
#
# Multi-step pipeline:
# pipeline = MultiStepPipeline.new
# result = pipeline.forward(input_data)
#
# With error handling:
# module = RobustModule.new
# begin
# result = module.forward(input_data)
# rescue DSPy::ValidationError => e
# puts "Failed after retries: #{e.message}"
# end
#
# Agent with tools:
# agent = AgentModule.new
# result = agent.forward(task: "Find the population of Tokyo")
#
# Stateful processing:
# module = StatefulModule.new
# result1 = module.forward("First input")
# result2 = module.forward("Second input") # Has context from first
# module.reset! # Clear history
#
# With caching:
# module = CachedModule.new
# result1 = module.forward(input) # Computes result
# result2 = module.forward(input) # Returns cached result

View File

@@ -1,143 +0,0 @@
# frozen_string_literal: true
# Example DSPy Signature Template
# This template demonstrates best practices for creating type-safe signatures
class ExampleSignature < DSPy::Signature
# Clear, specific description of what this signature does
# Good: "Classify customer support emails into Technical, Billing, or General categories"
# Avoid: "Classify emails"
description "Describe what this signature accomplishes and what output it produces"
# Input fields: Define what data the LLM receives
input do
# Basic field with description
const :field_name, String, desc: "Clear description of this input field"
# Numeric fields
const :count, Integer, desc: "Number of items to process"
const :score, Float, desc: "Confidence score between 0.0 and 1.0"
# Boolean fields
const :is_active, T::Boolean, desc: "Whether the item is currently active"
# Array fields
const :tags, T::Array[String], desc: "List of tags associated with the item"
# Optional: Enum for constrained values
const :priority, T.enum(["Low", "Medium", "High"]), desc: "Priority level"
end
# Output fields: Define what data the LLM produces
output do
# Primary output
const :result, String, desc: "The main result of the operation"
# Classification result with enum
const :category, T.enum(["Technical", "Billing", "General"]),
desc: "Category classification - must be one of: Technical, Billing, General"
# Confidence/metadata
const :confidence, Float, desc: "Confidence score (0.0-1.0) for this classification"
# Optional reasoning (automatically added by ChainOfThought)
# const :reasoning, String, desc: "Step-by-step reasoning for the classification"
end
end
# Example with multimodal input (vision)
class VisionExampleSignature < DSPy::Signature
description "Analyze an image and answer questions about its content"
input do
const :image, DSPy::Image, desc: "The image to analyze"
const :question, String, desc: "Question about the image content"
end
output do
const :answer, String, desc: "Detailed answer to the question about the image"
const :confidence, Float, desc: "Confidence in the answer (0.0-1.0)"
end
end
# Example for complex analysis task
class SentimentAnalysisSignature < DSPy::Signature
description "Analyze the sentiment of text with nuanced emotion detection"
input do
const :text, String, desc: "The text to analyze for sentiment"
const :context, String, desc: "Additional context about the text source or situation"
end
output do
const :sentiment, T.enum(["Positive", "Negative", "Neutral", "Mixed"]),
desc: "Overall sentiment - must be Positive, Negative, Neutral, or Mixed"
const :emotions, T::Array[String],
desc: "List of specific emotions detected (e.g., joy, anger, sadness, fear)"
const :intensity, T.enum(["Low", "Medium", "High"]),
desc: "Intensity of the detected sentiment"
const :confidence, Float,
desc: "Confidence in the sentiment classification (0.0-1.0)"
end
end
# Example for code generation task
class CodeGenerationSignature < DSPy::Signature
description "Generate Ruby code based on natural language requirements"
input do
const :requirements, String,
desc: "Natural language description of what the code should do"
const :constraints, String,
desc: "Any specific requirements or constraints (e.g., libraries to use, style preferences)"
end
output do
const :code, String,
desc: "Complete, working Ruby code that fulfills the requirements"
const :explanation, String,
desc: "Brief explanation of how the code works and any important design decisions"
const :dependencies, T::Array[String],
desc: "List of required gems or dependencies"
end
end
# Usage Examples:
#
# Basic usage with Predict:
# predictor = DSPy::Predict.new(ExampleSignature)
# result = predictor.forward(
# field_name: "example value",
# count: 5,
# score: 0.85,
# is_active: true,
# tags: ["tag1", "tag2"],
# priority: "High"
# )
# puts result[:result]
# puts result[:category]
# puts result[:confidence]
#
# With Chain of Thought reasoning:
# predictor = DSPy::ChainOfThought.new(SentimentAnalysisSignature)
# result = predictor.forward(
# text: "I absolutely love this product! It exceeded all my expectations.",
# context: "Product review on e-commerce site"
# )
# puts result[:reasoning] # See the LLM's step-by-step thinking
# puts result[:sentiment]
# puts result[:emotions]
#
# With Vision:
# predictor = DSPy::Predict.new(VisionExampleSignature)
# result = predictor.forward(
# image: DSPy::Image.from_file("path/to/image.jpg"),
# question: "What objects are visible in this image?"
# )
# puts result[:answer]

View File

@@ -1,265 +0,0 @@
# DSPy.rb Core Concepts
## Philosophy
DSPy.rb enables developers to **program LLMs, not prompt them**. Instead of manually crafting prompts, define application requirements through code using type-safe, composable modules.
## Signatures
Signatures define type-safe input/output contracts for LLM operations. They specify what data goes in and what data comes out, with runtime type checking.
### Basic Signature Structure
```ruby
class TaskSignature < DSPy::Signature
description "Brief description of what this signature does"
input do
const :field_name, String, desc: "Description of this input field"
const :another_field, Integer, desc: "Another input field"
end
output do
const :result_field, String, desc: "Description of the output"
const :confidence, Float, desc: "Confidence score (0.0-1.0)"
end
end
```
### Type Safety
Signatures support Sorbet types including:
- `String` - Text data
- `Integer`, `Float` - Numeric data
- `T::Boolean` - Boolean values
- `T::Array[Type]` - Arrays of specific types
- Custom enums and classes
### Field Descriptions
Always provide clear field descriptions using the `desc:` parameter. These descriptions:
- Guide the LLM on expected input/output format
- Serve as documentation for developers
- Improve prediction accuracy
## Modules
Modules are composable building blocks that use signatures to perform LLM operations. They can be chained together to create complex workflows.
### Basic Module Structure
```ruby
class MyModule < DSPy::Module
def initialize
super
@predictor = DSPy::Predict.new(MySignature)
end
def forward(input_hash)
@predictor.forward(input_hash)
end
end
```
### Module Composition
Modules can call other modules to create pipelines:
```ruby
class ComplexWorkflow < DSPy::Module
def initialize
super
@step1 = FirstModule.new
@step2 = SecondModule.new
end
def forward(input)
result1 = @step1.forward(input)
result2 = @step2.forward(result1)
result2
end
end
```
## Predictors
Predictors are the core execution engines that take signatures and perform LLM inference. DSPy.rb provides several predictor types.
### Predict
Basic LLM inference with type-safe inputs and outputs.
```ruby
predictor = DSPy::Predict.new(TaskSignature)
result = predictor.forward(field_name: "value", another_field: 42)
# Returns: { result_field: "...", confidence: 0.85 }
```
### ChainOfThought
Automatically adds a reasoning field to the output, improving accuracy for complex tasks.
```ruby
class EmailClassificationSignature < DSPy::Signature
description "Classify customer support emails"
input do
const :email_subject, String
const :email_body, String
end
output do
const :category, String # "Technical", "Billing", or "General"
const :priority, String # "High", "Medium", or "Low"
end
end
predictor = DSPy::ChainOfThought.new(EmailClassificationSignature)
result = predictor.forward(
email_subject: "Can't log in to my account",
email_body: "I've been trying to access my account for hours..."
)
# Returns: {
# reasoning: "This appears to be a technical issue...",
# category: "Technical",
# priority: "High"
# }
```
### ReAct
Tool-using agents with iterative reasoning. Enables autonomous problem-solving by allowing the LLM to use external tools.
```ruby
class SearchTool < DSPy::Tool
def call(query:)
# Perform search and return results
{ results: search_database(query) }
end
end
predictor = DSPy::ReAct.new(
TaskSignature,
tools: [SearchTool.new],
max_iterations: 5
)
```
### CodeAct
Dynamic code generation for solving problems programmatically. Requires the optional `dspy-code_act` gem.
```ruby
predictor = DSPy::CodeAct.new(TaskSignature)
result = predictor.forward(task: "Calculate the factorial of 5")
# The LLM generates and executes Ruby code to solve the task
```
## Multimodal Support
DSPy.rb supports vision capabilities across compatible models using the unified `DSPy::Image` interface.
```ruby
class VisionSignature < DSPy::Signature
description "Describe what's in an image"
input do
const :image, DSPy::Image
const :question, String
end
output do
const :description, String
end
end
predictor = DSPy::Predict.new(VisionSignature)
result = predictor.forward(
image: DSPy::Image.from_file("path/to/image.jpg"),
question: "What objects are visible in this image?"
)
```
### Image Input Methods
```ruby
# From file path
DSPy::Image.from_file("path/to/image.jpg")
# From URL (OpenAI only)
DSPy::Image.from_url("https://example.com/image.jpg")
# From base64-encoded data
DSPy::Image.from_base64(base64_string, mime_type: "image/jpeg")
```
## Best Practices
### 1. Clear Signature Descriptions
Always provide clear, specific descriptions for signatures and fields:
```ruby
# Good
description "Classify customer support emails into Technical, Billing, or General categories"
# Avoid
description "Classify emails"
```
### 2. Type Safety
Use specific types rather than generic String when possible:
```ruby
# Good - Use enums for constrained outputs
output do
const :category, T.enum(["Technical", "Billing", "General"])
end
# Less ideal - Generic string
output do
const :category, String, desc: "Must be Technical, Billing, or General"
end
```
### 3. Composable Architecture
Build complex workflows from simple, reusable modules:
```ruby
class EmailPipeline < DSPy::Module
def initialize
super
@classifier = EmailClassifier.new
@prioritizer = EmailPrioritizer.new
@responder = EmailResponder.new
end
def forward(email)
classification = @classifier.forward(email)
priority = @prioritizer.forward(classification)
@responder.forward(classification.merge(priority))
end
end
```
### 4. Error Handling
Always handle potential type validation errors:
```ruby
begin
result = predictor.forward(input_data)
rescue DSPy::ValidationError => e
# Handle validation error
logger.error "Invalid output from LLM: #{e.message}"
end
```
## Limitations
Current constraints to be aware of:
- No streaming support (single-request processing only)
- Limited multimodal support through Ollama for local deployments
- Vision capabilities vary by provider (see providers.md for compatibility matrix)

View File

@@ -1,623 +0,0 @@
# DSPy.rb Testing, Optimization & Observability
## Testing
DSPy.rb enables standard RSpec testing patterns for LLM logic, making your AI applications testable and maintainable.
### Basic Testing Setup
```ruby
require 'rspec'
require 'dspy'
RSpec.describe EmailClassifier do
before do
DSPy.configure do |c|
c.lm = DSPy::LM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY'])
end
end
describe '#classify' do
it 'classifies technical support emails correctly' do
classifier = EmailClassifier.new
result = classifier.forward(
email_subject: "Can't log in",
email_body: "I'm unable to access my account"
)
expect(result[:category]).to eq('Technical')
expect(result[:priority]).to be_in(['High', 'Medium', 'Low'])
end
end
end
```
### Mocking LLM Responses
Test your modules without making actual API calls:
```ruby
RSpec.describe MyModule do
it 'handles mock responses correctly' do
# Create a mock predictor that returns predetermined results
mock_predictor = instance_double(DSPy::Predict)
allow(mock_predictor).to receive(:forward).and_return({
category: 'Technical',
priority: 'High',
confidence: 0.95
})
# Inject mock into your module
module_instance = MyModule.new
module_instance.instance_variable_set(:@predictor, mock_predictor)
result = module_instance.forward(input: 'test data')
expect(result[:category]).to eq('Technical')
end
end
```
### Testing Type Safety
Verify that signatures enforce type constraints:
```ruby
RSpec.describe EmailClassificationSignature do
it 'validates output types' do
predictor = DSPy::Predict.new(EmailClassificationSignature)
# This should work
result = predictor.forward(
email_subject: 'Test',
email_body: 'Test body'
)
expect(result[:category]).to be_a(String)
# Test that invalid types are caught
expect {
# Simulate LLM returning invalid type
predictor.send(:validate_output, { category: 123 })
}.to raise_error(DSPy::ValidationError)
end
end
```
### Testing Edge Cases
Always test boundary conditions and error scenarios:
```ruby
RSpec.describe EmailClassifier do
it 'handles empty emails' do
classifier = EmailClassifier.new
result = classifier.forward(
email_subject: '',
email_body: ''
)
# Define expected behavior for edge case
expect(result[:category]).to eq('General')
end
it 'handles very long emails' do
long_body = 'word ' * 10000
classifier = EmailClassifier.new
expect {
classifier.forward(
email_subject: 'Test',
email_body: long_body
)
}.not_to raise_error
end
it 'handles special characters' do
classifier = EmailClassifier.new
result = classifier.forward(
email_subject: 'Test <script>alert("xss")</script>',
email_body: 'Body with émojis 🎉 and spëcial çharacters'
)
expect(result[:category]).to be_in(['Technical', 'Billing', 'General'])
end
end
```
### Integration Testing
Test complete workflows end-to-end:
```ruby
RSpec.describe EmailProcessingPipeline do
it 'processes email through complete pipeline' do
pipeline = EmailProcessingPipeline.new
result = pipeline.forward(
email_subject: 'Billing question',
email_body: 'How do I update my payment method?'
)
# Verify the complete pipeline output
expect(result[:classification]).to eq('Billing')
expect(result[:priority]).to eq('Medium')
expect(result[:suggested_response]).to include('payment')
expect(result[:assigned_team]).to eq('billing_support')
end
end
```
### VCR for Deterministic Tests
Use VCR to record and replay API responses:
```ruby
require 'vcr'
VCR.configure do |config|
config.cassette_library_dir = 'spec/vcr_cassettes'
config.hook_into :webmock
config.filter_sensitive_data('<OPENAI_API_KEY>') { ENV['OPENAI_API_KEY'] }
end
RSpec.describe EmailClassifier do
it 'classifies emails consistently', :vcr do
VCR.use_cassette('email_classification') do
classifier = EmailClassifier.new
result = classifier.forward(
email_subject: 'Test subject',
email_body: 'Test body'
)
expect(result[:category]).to eq('Technical')
end
end
end
```
## Optimization
DSPy.rb provides powerful optimization capabilities to automatically improve your prompts and modules.
### MIPROv2 Optimization
MIPROv2 is an advanced multi-prompt optimization technique that uses bootstrap sampling, instruction generation, and Bayesian optimization.
```ruby
require 'dspy/mipro'
# Define your module to optimize
class EmailClassifier < DSPy::Module
def initialize
super
@predictor = DSPy::ChainOfThought.new(EmailClassificationSignature)
end
def forward(input)
@predictor.forward(input)
end
end
# Prepare training data
training_examples = [
{
input: { email_subject: "Can't log in", email_body: "Password reset not working" },
expected_output: { category: 'Technical', priority: 'High' }
},
{
input: { email_subject: "Billing question", email_body: "How much does premium cost?" },
expected_output: { category: 'Billing', priority: 'Medium' }
},
# Add more examples...
]
# Define evaluation metric
def accuracy_metric(example, prediction)
(example[:expected_output][:category] == prediction[:category]) ? 1.0 : 0.0
end
# Run optimization
optimizer = DSPy::MIPROv2.new(
metric: method(:accuracy_metric),
num_candidates: 10,
num_threads: 4
)
optimized_module = optimizer.compile(
EmailClassifier.new,
trainset: training_examples
)
# Use optimized module
result = optimized_module.forward(
email_subject: "New email",
email_body: "New email content"
)
```
### Bootstrap Few-Shot Learning
Automatically generate few-shot examples from your training data:
```ruby
require 'dspy/teleprompt'
# Create a teleprompter for few-shot optimization
teleprompter = DSPy::BootstrapFewShot.new(
metric: method(:accuracy_metric),
max_bootstrapped_demos: 5,
max_labeled_demos: 3
)
# Compile the optimized module
optimized = teleprompter.compile(
MyModule.new,
trainset: training_examples
)
```
### Custom Optimization Metrics
Define custom metrics for your specific use case:
```ruby
def custom_metric(example, prediction)
score = 0.0
# Category accuracy (60% weight)
score += 0.6 if example[:expected_output][:category] == prediction[:category]
# Priority accuracy (40% weight)
score += 0.4 if example[:expected_output][:priority] == prediction[:priority]
score
end
# Use in optimization
optimizer = DSPy::MIPROv2.new(
metric: method(:custom_metric),
num_candidates: 10
)
```
### A/B Testing Different Approaches
Compare different module implementations:
```ruby
# Approach A: ChainOfThought
class ApproachA < DSPy::Module
def initialize
super
@predictor = DSPy::ChainOfThought.new(EmailClassificationSignature)
end
def forward(input)
@predictor.forward(input)
end
end
# Approach B: ReAct with tools
class ApproachB < DSPy::Module
def initialize
super
@predictor = DSPy::ReAct.new(
EmailClassificationSignature,
tools: [KnowledgeBaseTool.new]
)
end
def forward(input)
@predictor.forward(input)
end
end
# Evaluate both approaches
def evaluate_approach(approach_class, test_set)
approach = approach_class.new
scores = test_set.map do |example|
prediction = approach.forward(example[:input])
accuracy_metric(example, prediction)
end
scores.sum / scores.size
end
approach_a_score = evaluate_approach(ApproachA, test_examples)
approach_b_score = evaluate_approach(ApproachB, test_examples)
puts "Approach A accuracy: #{approach_a_score}"
puts "Approach B accuracy: #{approach_b_score}"
```
## Observability
Track your LLM application's performance, token usage, and behavior in production.
### OpenTelemetry Integration
DSPy.rb automatically integrates with OpenTelemetry when configured:
```ruby
require 'opentelemetry/sdk'
require 'dspy'
# Configure OpenTelemetry
OpenTelemetry::SDK.configure do |c|
c.service_name = 'my-dspy-app'
c.use_all # Use all available instrumentation
end
# DSPy automatically creates traces for predictions
predictor = DSPy::Predict.new(MySignature)
result = predictor.forward(input: 'data')
# Traces are automatically sent to your OpenTelemetry collector
```
### Langfuse Integration
Track detailed LLM execution traces with Langfuse:
```ruby
require 'dspy/langfuse'
# Configure Langfuse
DSPy.configure do |c|
c.lm = DSPy::LM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY'])
c.langfuse = {
public_key: ENV['LANGFUSE_PUBLIC_KEY'],
secret_key: ENV['LANGFUSE_SECRET_KEY'],
host: ENV['LANGFUSE_HOST'] || 'https://cloud.langfuse.com'
}
end
# All predictions are automatically traced
predictor = DSPy::Predict.new(MySignature)
result = predictor.forward(input: 'data')
# View detailed traces in Langfuse dashboard
```
### Manual Token Tracking
Track token usage without external services:
```ruby
class TokenTracker
def initialize
@total_tokens = 0
@request_count = 0
end
def track_prediction(predictor, input)
start_time = Time.now
result = predictor.forward(input)
duration = Time.now - start_time
# Get token usage from response metadata
tokens = result.metadata[:usage][:total_tokens] rescue 0
@total_tokens += tokens
@request_count += 1
puts "Request ##{@request_count}: #{tokens} tokens in #{duration}s"
puts "Total tokens used: #{@total_tokens}"
result
end
end
# Usage
tracker = TokenTracker.new
predictor = DSPy::Predict.new(MySignature)
result = tracker.track_prediction(predictor, { input: 'data' })
```
### Custom Logging
Add detailed logging to your modules:
```ruby
class EmailClassifier < DSPy::Module
def initialize
super
@predictor = DSPy::ChainOfThought.new(EmailClassificationSignature)
@logger = Logger.new(STDOUT)
end
def forward(input)
@logger.info "Classifying email: #{input[:email_subject]}"
start_time = Time.now
result = @predictor.forward(input)
duration = Time.now - start_time
@logger.info "Classification: #{result[:category]} (#{duration}s)"
if result[:reasoning]
@logger.debug "Reasoning: #{result[:reasoning]}"
end
result
rescue => e
@logger.error "Classification failed: #{e.message}"
raise
end
end
```
### Performance Monitoring
Monitor latency and performance metrics:
```ruby
class PerformanceMonitor
def initialize
@metrics = {
total_requests: 0,
total_duration: 0.0,
errors: 0,
success_count: 0
}
end
def monitor_request
start_time = Time.now
@metrics[:total_requests] += 1
begin
result = yield
@metrics[:success_count] += 1
result
rescue => e
@metrics[:errors] += 1
raise
ensure
duration = Time.now - start_time
@metrics[:total_duration] += duration
if @metrics[:total_requests] % 10 == 0
print_stats
end
end
end
def print_stats
avg_duration = @metrics[:total_duration] / @metrics[:total_requests]
success_rate = @metrics[:success_count].to_f / @metrics[:total_requests]
puts "\n=== Performance Stats ==="
puts "Total requests: #{@metrics[:total_requests]}"
puts "Average duration: #{avg_duration.round(3)}s"
puts "Success rate: #{(success_rate * 100).round(2)}%"
puts "Errors: #{@metrics[:errors]}"
puts "========================\n"
end
end
# Usage
monitor = PerformanceMonitor.new
predictor = DSPy::Predict.new(MySignature)
result = monitor.monitor_request do
predictor.forward(input: 'data')
end
```
### Error Rate Tracking
Monitor and alert on error rates:
```ruby
class ErrorRateMonitor
def initialize(alert_threshold: 0.1)
@alert_threshold = alert_threshold
@recent_results = []
@window_size = 100
end
def track_result(success:)
@recent_results << success
@recent_results.shift if @recent_results.size > @window_size
error_rate = calculate_error_rate
alert_if_needed(error_rate)
error_rate
end
private
def calculate_error_rate
failures = @recent_results.count(false)
failures.to_f / @recent_results.size
end
def alert_if_needed(error_rate)
if error_rate > @alert_threshold
puts "⚠️ ALERT: Error rate #{(error_rate * 100).round(2)}% exceeds threshold!"
# Send notification, page oncall, etc.
end
end
end
```
## Best Practices
### 1. Start with Tests
Write tests before optimizing:
```ruby
# Define test cases first
test_cases = [
{ input: {...}, expected: {...} },
# More test cases...
]
# Ensure baseline functionality
test_cases.each do |tc|
result = module.forward(tc[:input])
assert result[:category] == tc[:expected][:category]
end
# Then optimize
optimized = optimizer.compile(module, trainset: test_cases)
```
### 2. Use Meaningful Metrics
Define metrics that align with business goals:
```ruby
def business_aligned_metric(example, prediction)
# High-priority errors are more costly
if example[:expected_output][:priority] == 'High'
return prediction[:priority] == 'High' ? 1.0 : 0.0
else
return prediction[:category] == example[:expected_output][:category] ? 0.8 : 0.0
end
end
```
### 3. Monitor in Production
Always track production performance:
```ruby
class ProductionModule < DSPy::Module
def initialize
super
@predictor = DSPy::ChainOfThought.new(MySignature)
@monitor = PerformanceMonitor.new
@error_tracker = ErrorRateMonitor.new
end
def forward(input)
@monitor.monitor_request do
result = @predictor.forward(input)
@error_tracker.track_result(success: true)
result
rescue => e
@error_tracker.track_result(success: false)
raise
end
end
end
```
### 4. Version Your Modules
Track which version of your module is deployed:
```ruby
class EmailClassifierV2 < DSPy::Module
VERSION = '2.1.0'
def initialize
super
@predictor = DSPy::ChainOfThought.new(EmailClassificationSignature)
end
def forward(input)
result = @predictor.forward(input)
result.merge(model_version: VERSION)
end
end
```

View File

@@ -1,338 +0,0 @@
# DSPy.rb LLM Providers
## Supported Providers
DSPy.rb provides unified support across multiple LLM providers through adapter gems that automatically load when installed.
### Provider Overview
- **OpenAI**: GPT-4, GPT-4o, GPT-4o-mini, GPT-3.5-turbo
- **Anthropic**: Claude 3 family (Sonnet, Opus, Haiku), Claude 3.5 Sonnet
- **Google Gemini**: Gemini 1.5 Pro, Gemini 1.5 Flash, other versions
- **Ollama**: Local model support via OpenAI compatibility layer
- **OpenRouter**: Unified multi-provider API for 200+ models
## Configuration
### Basic Setup
```ruby
require 'dspy'
DSPy.configure do |c|
c.lm = DSPy::LM.new('provider/model-name', api_key: ENV['API_KEY'])
end
```
### OpenAI Configuration
**Required gem**: `dspy-openai`
```ruby
DSPy.configure do |c|
# GPT-4o Mini (recommended for development)
c.lm = DSPy::LM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY'])
# GPT-4o (more capable)
c.lm = DSPy::LM.new('openai/gpt-4o', api_key: ENV['OPENAI_API_KEY'])
# GPT-4 Turbo
c.lm = DSPy::LM.new('openai/gpt-4-turbo', api_key: ENV['OPENAI_API_KEY'])
end
```
**Environment variable**: `OPENAI_API_KEY`
### Anthropic Configuration
**Required gem**: `dspy-anthropic`
```ruby
DSPy.configure do |c|
# Claude 3.5 Sonnet (latest, most capable)
c.lm = DSPy::LM.new('anthropic/claude-3-5-sonnet-20241022',
api_key: ENV['ANTHROPIC_API_KEY'])
# Claude 3 Opus (most capable in Claude 3 family)
c.lm = DSPy::LM.new('anthropic/claude-3-opus-20240229',
api_key: ENV['ANTHROPIC_API_KEY'])
# Claude 3 Sonnet (balanced)
c.lm = DSPy::LM.new('anthropic/claude-3-sonnet-20240229',
api_key: ENV['ANTHROPIC_API_KEY'])
# Claude 3 Haiku (fast, cost-effective)
c.lm = DSPy::LM.new('anthropic/claude-3-haiku-20240307',
api_key: ENV['ANTHROPIC_API_KEY'])
end
```
**Environment variable**: `ANTHROPIC_API_KEY`
### Google Gemini Configuration
**Required gem**: `dspy-gemini`
```ruby
DSPy.configure do |c|
# Gemini 1.5 Pro (most capable)
c.lm = DSPy::LM.new('gemini/gemini-1.5-pro',
api_key: ENV['GOOGLE_API_KEY'])
# Gemini 1.5 Flash (faster, cost-effective)
c.lm = DSPy::LM.new('gemini/gemini-1.5-flash',
api_key: ENV['GOOGLE_API_KEY'])
end
```
**Environment variable**: `GOOGLE_API_KEY` or `GEMINI_API_KEY`
### Ollama Configuration
**Required gem**: None (uses OpenAI compatibility layer)
```ruby
DSPy.configure do |c|
# Local Ollama instance
c.lm = DSPy::LM.new('ollama/llama3.1',
base_url: 'http://localhost:11434')
# Other Ollama models
c.lm = DSPy::LM.new('ollama/mistral')
c.lm = DSPy::LM.new('ollama/codellama')
end
```
**Note**: Ensure Ollama is running locally: `ollama serve`
### OpenRouter Configuration
**Required gem**: `dspy-openai` (uses OpenAI adapter)
```ruby
DSPy.configure do |c|
# Access 200+ models through OpenRouter
c.lm = DSPy::LM.new('openrouter/anthropic/claude-3.5-sonnet',
api_key: ENV['OPENROUTER_API_KEY'],
base_url: 'https://openrouter.ai/api/v1')
# Other examples
c.lm = DSPy::LM.new('openrouter/google/gemini-pro')
c.lm = DSPy::LM.new('openrouter/meta-llama/llama-3.1-70b-instruct')
end
```
**Environment variable**: `OPENROUTER_API_KEY`
## Provider Compatibility Matrix
### Feature Support
| Feature | OpenAI | Anthropic | Gemini | Ollama |
|---------|--------|-----------|--------|--------|
| Structured Output | ✅ | ✅ | ✅ | ✅ |
| Vision (Images) | ✅ | ✅ | ✅ | ⚠️ Limited |
| Image URLs | ✅ | ❌ | ❌ | ❌ |
| Tool Calling | ✅ | ✅ | ✅ | Varies |
| Streaming | ❌ | ❌ | ❌ | ❌ |
| Function Calling | ✅ | ✅ | ✅ | Varies |
**Legend**: ✅ Full support | ⚠️ Partial support | ❌ Not supported
### Vision Capabilities
**Image URLs**: Only OpenAI supports direct URL references. For other providers, load images as base64 or from files.
```ruby
# OpenAI - supports URLs
DSPy::Image.from_url("https://example.com/image.jpg")
# Anthropic, Gemini - use file or base64
DSPy::Image.from_file("path/to/image.jpg")
DSPy::Image.from_base64(base64_data, mime_type: "image/jpeg")
```
**Ollama**: Limited multimodal functionality. Check specific model capabilities.
## Advanced Configuration
### Custom Parameters
Pass provider-specific parameters during configuration:
```ruby
DSPy.configure do |c|
c.lm = DSPy::LM.new('openai/gpt-4o',
api_key: ENV['OPENAI_API_KEY'],
temperature: 0.7,
max_tokens: 2000,
top_p: 0.9
)
end
```
### Multiple Providers
Use different models for different tasks:
```ruby
# Fast model for simple tasks
fast_lm = DSPy::LM.new('openai/gpt-4o-mini', api_key: ENV['OPENAI_API_KEY'])
# Powerful model for complex tasks
powerful_lm = DSPy::LM.new('anthropic/claude-3-5-sonnet-20241022',
api_key: ENV['ANTHROPIC_API_KEY'])
# Use different models in different modules
class SimpleClassifier < DSPy::Module
def initialize
super
DSPy.configure { |c| c.lm = fast_lm }
@predictor = DSPy::Predict.new(SimpleSignature)
end
end
class ComplexAnalyzer < DSPy::Module
def initialize
super
DSPy.configure { |c| c.lm = powerful_lm }
@predictor = DSPy::ChainOfThought.new(ComplexSignature)
end
end
```
### Per-Request Configuration
Override configuration for specific predictions:
```ruby
predictor = DSPy::Predict.new(MySignature)
# Use default configuration
result1 = predictor.forward(input: "data")
# Override temperature for this request
result2 = predictor.forward(
input: "data",
config: { temperature: 0.2 } # More deterministic
)
```
## Cost Optimization
### Model Selection Strategy
1. **Development**: Use cheaper, faster models (gpt-4o-mini, claude-3-haiku, gemini-1.5-flash)
2. **Production Simple Tasks**: Continue with cheaper models if quality is sufficient
3. **Production Complex Tasks**: Upgrade to more capable models (gpt-4o, claude-3.5-sonnet, gemini-1.5-pro)
4. **Local Development**: Use Ollama for privacy and zero API costs
### Example Cost-Conscious Setup
```ruby
# Development environment
if Rails.env.development?
DSPy.configure do |c|
c.lm = DSPy::LM.new('ollama/llama3.1') # Free, local
end
elsif Rails.env.test?
DSPy.configure do |c|
c.lm = DSPy::LM.new('openai/gpt-4o-mini', # Cheap for testing
api_key: ENV['OPENAI_API_KEY'])
end
else # production
DSPy.configure do |c|
c.lm = DSPy::LM.new('anthropic/claude-3-5-sonnet-20241022',
api_key: ENV['ANTHROPIC_API_KEY'])
end
end
```
## Provider-Specific Best Practices
### OpenAI
- Use `gpt-4o-mini` for development and simple tasks
- Use `gpt-4o` for production complex tasks
- Best vision support including URL loading
- Excellent function calling capabilities
### Anthropic
- Claude 3.5 Sonnet is currently the most capable model
- Excellent for complex reasoning and analysis
- Strong safety features and helpful outputs
- Requires base64 for images (no URL support)
### Google Gemini
- Gemini 1.5 Pro for complex tasks, Flash for speed
- Strong multimodal capabilities
- Good balance of cost and performance
- Requires base64 for images
### Ollama
- Best for privacy-sensitive applications
- Zero API costs
- Requires local hardware resources
- Limited multimodal support depending on model
- Good for development and testing
## Troubleshooting
### API Key Issues
```ruby
# Verify API key is set
if ENV['OPENAI_API_KEY'].nil?
raise "OPENAI_API_KEY environment variable not set"
end
# Test connection
begin
DSPy.configure { |c| c.lm = DSPy::LM.new('openai/gpt-4o-mini',
api_key: ENV['OPENAI_API_KEY']) }
predictor = DSPy::Predict.new(TestSignature)
predictor.forward(test: "data")
puts "✅ Connection successful"
rescue => e
puts "❌ Connection failed: #{e.message}"
end
```
### Rate Limiting
Handle rate limits gracefully:
```ruby
def call_with_retry(predictor, input, max_retries: 3)
retries = 0
begin
predictor.forward(input)
rescue RateLimitError => e
retries += 1
if retries < max_retries
sleep(2 ** retries) # Exponential backoff
retry
else
raise
end
end
end
```
### Model Not Found
Ensure the correct gem is installed:
```bash
# For OpenAI
gem install dspy-openai
# For Anthropic
gem install dspy-anthropic
# For Gemini
gem install dspy-gemini
```

View File

@@ -0,0 +1,221 @@
---
name: fastapi-style
description: This skill should be used when writing Python and FastAPI code following opinionated best practices. It applies when building APIs, creating Pydantic models, working with SQLAlchemy, or any FastAPI application. Triggers on FastAPI code generation, API design, refactoring requests, code review, or when discussing async Python patterns. Embodies thin routers, rich Pydantic models, dependency injection, async-first design, and the "explicit is better than implicit" philosophy.
---
<objective>
Apply opinionated FastAPI conventions to Python API code. This skill provides comprehensive domain expertise for building maintainable, performant FastAPI applications following established patterns from production codebases.
</objective>
<essential_principles>
## Core Philosophy
"Explicit is better than implicit. Simple is better than complex."
**The FastAPI Way:**
- Thin routers, rich Pydantic models with validation
- Dependency injection for everything
- Async-first with SQLAlchemy 2.0
- Type hints everywhere - let the tools help you
- Settings via pydantic-settings, not raw env vars
- Database-backed solutions where possible
**What to deliberately avoid:**
- Flask patterns (global request context)
- Django ORM in FastAPI (use SQLAlchemy 2.0)
- Synchronous database calls (use async)
- Manual JSON serialization (Pydantic handles it)
- Global state (use dependency injection)
- `*` imports (explicit imports only)
- Circular imports (proper module structure)
**Development Philosophy:**
- Type everything - mypy should pass
- Fail fast with descriptive errors
- Write-time validation over read-time checks
- Database constraints complement Pydantic validation
- Tests are documentation
</essential_principles>
<intake>
What are you working on?
1. **Routers** - Route organization, dependency injection, response models
2. **Models** - Pydantic schemas, SQLAlchemy models, validation patterns
3. **Database** - SQLAlchemy 2.0 async, Alembic migrations, transactions
4. **Testing** - pytest, httpx TestClient, fixtures, async testing
5. **Security** - OAuth2, JWT, permissions, CORS, rate limiting
6. **Background Tasks** - Celery, ARQ, or FastAPI BackgroundTasks
7. **Code Review** - Review code against FastAPI best practices
8. **General Guidance** - Philosophy and conventions
**Specify a number or describe your task.**
</intake>
<routing>
| Response | Reference to Read |
|----------|-------------------|
| 1, router, route, endpoint | [routers.md](./references/routers.md) |
| 2, model, pydantic, schema, sqlalchemy | [models.md](./references/models.md) |
| 3, database, db, alembic, migration, transaction | [database.md](./references/database.md) |
| 4, test, testing, pytest, fixture | [testing.md](./references/testing.md) |
| 5, security, auth, oauth, jwt, permission | [security.md](./references/security.md) |
| 6, background, task, celery, arq, queue | [background_tasks.md](./references/background_tasks.md) |
| 7, review | Read all references, then review code |
| 8, general task | Read relevant references based on context |
**After reading relevant references, apply patterns to the user's code.**
</routing>
<quick_reference>
## Project Structure
```
app/
├── main.py # FastAPI app creation, middleware
├── config.py # Settings via pydantic-settings
├── dependencies.py # Shared dependencies
├── database.py # Database session, engine
├── models/ # SQLAlchemy models
│ ├── __init__.py
│ ├── base.py # Base model class
│ └── user.py
├── schemas/ # Pydantic models
│ ├── __init__.py
│ └── user.py
├── routers/ # API routers
│ ├── __init__.py
│ └── users.py
├── services/ # Business logic (if needed)
├── utils/ # Shared utilities
└── tests/
├── conftest.py # Fixtures
└── test_users.py
```
## Naming Conventions
**Pydantic Schemas:**
- `UserCreate` - input for creation
- `UserUpdate` - input for updates (all fields Optional)
- `UserRead` - output representation
- `UserInDB` - internal with hashed password
**SQLAlchemy Models:** Singular nouns (`User`, `Item`, `Order`)
**Routers:** Plural resource names (`users.py`, `items.py`)
**Dependencies:** Verb phrases (`get_current_user`, `get_db_session`)
## Type Hints
```python
# Always type function signatures
async def get_user(
user_id: int,
db: AsyncSession = Depends(get_db),
) -> User:
...
# Use Annotated for dependency injection
from typing import Annotated
CurrentUser = Annotated[User, Depends(get_current_user)]
DBSession = Annotated[AsyncSession, Depends(get_db)]
```
## Response Patterns
```python
# Explicit response_model
@router.get("/users/{user_id}", response_model=UserRead)
async def get_user(user_id: int, db: DBSession) -> User:
...
# Status codes
@router.post("/users", status_code=status.HTTP_201_CREATED)
async def create_user(...) -> UserRead:
...
# Multiple response types
@router.get("/users/{user_id}", responses={404: {"model": ErrorResponse}})
```
## Error Handling
```python
from fastapi import HTTPException, status
# Specific exceptions
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
# Custom exception handlers
@app.exception_handler(ValidationError)
async def validation_exception_handler(request, exc):
return JSONResponse(status_code=422, content={"detail": exc.errors()})
```
## Dependency Injection
```python
# Simple dependency
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session:
yield session
# Parameterized dependency
def get_pagination(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
) -> dict:
return {"skip": skip, "limit": limit}
# Class-based dependency
class CommonQueryParams:
def __init__(self, q: str | None = None, skip: int = 0, limit: int = 100):
self.q = q
self.skip = skip
self.limit = limit
```
</quick_reference>
<reference_index>
## Domain Knowledge
All detailed patterns in `references/`:
| File | Topics |
|------|--------|
| [routers.md](./references/routers.md) | Route organization, dependency injection, response models, middleware, versioning |
| [models.md](./references/models.md) | Pydantic schemas, SQLAlchemy models, validation, serialization, mixins |
| [database.md](./references/database.md) | SQLAlchemy 2.0 async, Alembic migrations, transactions, connection pooling |
| [testing.md](./references/testing.md) | pytest, httpx TestClient, fixtures, async testing, mocking patterns |
| [security.md](./references/security.md) | OAuth2, JWT, permissions, CORS, rate limiting, secrets management |
| [background_tasks.md](./references/background_tasks.md) | FastAPI BackgroundTasks, Celery, ARQ, task patterns |
</reference_index>
<success_criteria>
Code follows FastAPI best practices when:
- Routers are thin, focused on HTTP concerns only
- Pydantic models handle all validation and serialization
- SQLAlchemy 2.0 async patterns used correctly
- Dependencies injected, not imported as globals
- Type hints on all function signatures
- Settings via pydantic-settings
- Tests use pytest with async support
- Error handling is explicit and informative
- Security follows OAuth2/JWT standards
- Background tasks use appropriate tool for the job
</success_criteria>
<credits>
Based on FastAPI best practices from the official documentation, real-world production patterns, and the Python community's collective wisdom.
**Key Resources:**
- [FastAPI Documentation](https://fastapi.tiangolo.com/)
- [SQLAlchemy 2.0 Documentation](https://docs.sqlalchemy.org/)
- [Pydantic V2 Documentation](https://docs.pydantic.dev/)
</credits>

View File

@@ -0,0 +1,369 @@
---
name: python-package-writer
description: This skill should be used when writing Python packages following production-ready patterns and philosophy. It applies when creating new Python packages, refactoring existing packages, designing package APIs, or when clean, minimal, well-tested Python library code is needed. Triggers on requests like "create a package", "write a Python library", "design a package API", or mentions of PyPI publishing.
---
# Python Package Writer
Write Python packages following battle-tested patterns from production-ready libraries. Emphasis on simplicity, minimal dependencies, comprehensive testing, and modern packaging standards (pyproject.toml, type hints, pytest).
## Core Philosophy
**Simplicity over cleverness.** Zero or minimal dependencies. Explicit code over magic. Framework integration without framework coupling. Every pattern serves production use cases.
## Package Structure (src layout)
The modern recommended layout with proper namespace isolation:
```
package-name/
├── pyproject.toml # All metadata and configuration
├── README.md
├── LICENSE
├── py.typed # PEP 561 marker for type hints
├── src/
│ └── package_name/ # Actual package code
│ ├── __init__.py # Entry point, exports, version
│ ├── core.py # Core functionality
│ ├── models.py # Data models (Pydantic/dataclasses)
│ ├── exceptions.py # Custom exceptions
│ └── py.typed # Type hint marker (also here)
└── tests/
├── conftest.py # Pytest fixtures
├── test_core.py
└── test_models.py
```
## Entry Point Structure
Every package follows this pattern in `src/package_name/__init__.py`:
```python
"""Package description - one line."""
# Public API exports
from package_name.core import Client, process_data
from package_name.models import Config, Result
from package_name.exceptions import PackageError, ValidationError
__version__ = "1.0.0"
__all__ = [
"Client",
"process_data",
"Config",
"Result",
"PackageError",
"ValidationError",
]
```
## pyproject.toml Configuration
Modern packaging with all metadata in one file:
```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "package-name"
version = "1.0.0"
description = "Brief description of what the package does"
readme = "README.md"
license = "MIT"
requires-python = ">=3.10"
authors = [
{ name = "Your Name", email = "you@example.com" }
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Typing :: Typed",
]
keywords = ["keyword1", "keyword2"]
# Zero or minimal runtime dependencies
dependencies = []
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-cov>=4.0",
"ruff>=0.4",
"mypy>=1.0",
]
# Optional integrations
fastapi = ["fastapi>=0.100", "pydantic>=2.0"]
[project.urls]
Homepage = "https://github.com/username/package-name"
Documentation = "https://package-name.readthedocs.io"
Repository = "https://github.com/username/package-name"
Changelog = "https://github.com/username/package-name/blob/main/CHANGELOG.md"
[tool.hatch.build.targets.wheel]
packages = ["src/package_name"]
[tool.ruff]
target-version = "py310"
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP", "B", "C4", "SIM"]
[tool.mypy]
python_version = "3.10"
strict = true
warn_return_any = true
warn_unused_ignores = true
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra -q"
[tool.coverage.run]
source = ["src/package_name"]
branch = true
```
## Configuration Pattern
Use module-level configuration with dataclasses or simple attributes:
```python
# src/package_name/config.py
from dataclasses import dataclass, field
from os import environ
from typing import Any
@dataclass
class Config:
"""Package configuration with sensible defaults."""
timeout: int = 30
retries: int = 3
api_key: str | None = field(default=None)
debug: bool = False
def __post_init__(self) -> None:
# Environment variable fallbacks
if self.api_key is None:
self.api_key = environ.get("PACKAGE_API_KEY")
# Module-level singleton (optional)
_config: Config | None = None
def get_config() -> Config:
"""Get or create the global config instance."""
global _config
if _config is None:
_config = Config()
return _config
def configure(**kwargs: Any) -> Config:
"""Configure the package with custom settings."""
global _config
_config = Config(**kwargs)
return _config
```
## Error Handling
Simple hierarchy with informative messages:
```python
# src/package_name/exceptions.py
class PackageError(Exception):
"""Base exception for all package errors."""
pass
class ConfigError(PackageError):
"""Invalid configuration."""
pass
class ValidationError(PackageError):
"""Data validation failed."""
def __init__(self, message: str, field: str | None = None) -> None:
self.field = field
super().__init__(message)
class APIError(PackageError):
"""External API error."""
def __init__(self, message: str, status_code: int | None = None) -> None:
self.status_code = status_code
super().__init__(message)
# Validate early with ValueError
def process(data: bytes) -> str:
if not data:
raise ValueError("Data cannot be empty")
if len(data) > 1_000_000:
raise ValueError(f"Data too large: {len(data)} bytes (max 1MB)")
return data.decode("utf-8")
```
## Type Hints
Always use type hints with modern syntax (Python 3.10+):
```python
# Use built-in generics, not typing module
from collections.abc import Callable, Iterator, Mapping, Sequence
def process_items(
items: list[str],
transform: Callable[[str], str] | None = None,
*,
batch_size: int = 100,
) -> Iterator[str]:
"""Process items with optional transformation."""
for item in items:
if transform:
yield transform(item)
else:
yield item
# Use | for unions, not Union
def get_value(key: str) -> str | None:
return _cache.get(key)
# Use Self for return type annotations (Python 3.11+)
from typing import Self
class Client:
def configure(self, **kwargs: str) -> Self:
# Update configuration
return self
```
## Testing (pytest)
```python
# tests/conftest.py
import pytest
from package_name import Config, configure
@pytest.fixture
def config() -> Config:
"""Fresh config for each test."""
return configure(timeout=5, debug=True)
@pytest.fixture
def sample_data() -> bytes:
"""Sample input data."""
return b"test data content"
# tests/test_core.py
import pytest
from package_name import process_data, PackageError
class TestProcessData:
"""Tests for process_data function."""
def test_basic_functionality(self, sample_data: bytes) -> None:
result = process_data(sample_data)
assert result == "test data content"
def test_empty_input_raises_error(self) -> None:
with pytest.raises(ValueError, match="cannot be empty"):
process_data(b"")
def test_with_transform(self, sample_data: bytes) -> None:
result = process_data(sample_data, transform=str.upper)
assert result == "TEST DATA CONTENT"
class TestConfig:
"""Tests for configuration."""
def test_defaults(self) -> None:
config = Config()
assert config.timeout == 30
assert config.retries == 3
def test_env_fallback(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("PACKAGE_API_KEY", "test-key")
config = Config()
assert config.api_key == "test-key"
```
## FastAPI Integration
Optional FastAPI integration pattern:
```python
# src/package_name/fastapi.py
"""FastAPI integration - only import if FastAPI is installed."""
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from fastapi import FastAPI
from package_name.config import get_config
def init_app(app: "FastAPI") -> None:
"""Initialize package with FastAPI app."""
config = get_config()
@app.on_event("startup")
async def startup() -> None:
# Initialize connections, caches, etc.
pass
@app.on_event("shutdown")
async def shutdown() -> None:
# Cleanup resources
pass
# Usage in FastAPI app:
# from package_name.fastapi import init_app
# init_app(app)
```
## Anti-Patterns to Avoid
- `__getattr__` magic (use explicit imports)
- Global mutable state (use configuration objects)
- `*` imports in `__init__.py` (explicit `__all__`)
- Many runtime dependencies
- Committing `.venv/` or `__pycache__/`
- Not including `py.typed` marker
- Using `setup.py` (use `pyproject.toml`)
- Mixing src layout and flat layout
- `print()` for debugging (use logging)
- Bare `except:` clauses
## Reference Files
For deeper patterns, see:
- **[references/package-structure.md](./references/package-structure.md)** - Directory layouts, module organization
- **[references/pyproject-config.md](./references/pyproject-config.md)** - Complete pyproject.toml examples
- **[references/testing-patterns.md](./references/testing-patterns.md)** - pytest patterns, fixtures, CI setup
- **[references/type-hints.md](./references/type-hints.md)** - Modern typing patterns
- **[references/fastapi-integration.md](./references/fastapi-integration.md)** - FastAPI/Pydantic integration
- **[references/publishing.md](./references/publishing.md)** - PyPI publishing, CI/CD
- **[references/resources.md](./references/resources.md)** - Links to exemplary Python packages