319 lines
8.7 KiB
Markdown
319 lines
8.7 KiB
Markdown
# Skill Composition Example
|
||
|
||
Real-world example of combining multiple skills for a complex task.
|
||
|
||
## Task: Implement Secure User Authentication API
|
||
|
||
**Requirement**: Add JWT-based authentication with secure password storage, comprehensive tests, and API documentation.
|
||
|
||
## Skills Involved
|
||
|
||
This task requires **7 different skills** working together:
|
||
|
||
1. **api-design** - REST API patterns
|
||
2. **security-patterns** - Authentication, JWT, password hashing
|
||
3. **database-design** - User table schema, query optimization
|
||
4. **python-standards** - Code style, type hints
|
||
5. **testing-guide** - Security testing patterns
|
||
6. **documentation-guide** - API documentation standards
|
||
7. **observability** - Authentication logging
|
||
|
||
## Progressive Disclosure in Action
|
||
|
||
### Context Load (Startup)
|
||
|
||
```
|
||
Agent context:
|
||
├── implementer agent prompt: ~500 tokens
|
||
├── 21 skill metadata: ~1,050 tokens
|
||
├── Task description: ~200 tokens
|
||
└── Total: ~1,750 tokens (< 1% of budget)
|
||
```
|
||
|
||
### Stage 1: API Design (Minutes 0-5)
|
||
|
||
**Agent thinks**: "Need to design authentication endpoints"
|
||
|
||
**Skill activates**: api-design (~4,000 tokens)
|
||
|
||
```python
|
||
# Agent designs endpoints following api-design skill
|
||
@app.post("/auth/login")
|
||
async def login(credentials: LoginRequest) -> TokenResponse:
|
||
pass
|
||
|
||
@app.post("/auth/refresh")
|
||
async def refresh_token(refresh: RefreshRequest) -> TokenResponse:
|
||
pass
|
||
|
||
@app.get("/auth/me")
|
||
async def get_current_user(user: User = Depends(get_current_user)) -> User:
|
||
pass
|
||
```
|
||
|
||
**Context**: ~5,750 tokens
|
||
|
||
### Stage 2: Security Implementation (Minutes 5-10)
|
||
|
||
**Agent thinks**: "Need JWT validation and password hashing"
|
||
|
||
**Skill activates**: security-patterns (~6,000 tokens)
|
||
|
||
```python
|
||
# Agent implements security following security-patterns skill
|
||
from passlib.context import CryptContext
|
||
from jose import JWTError, jwt
|
||
|
||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||
|
||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||
return pwd_context.verify(plain_password, hashed_password)
|
||
|
||
def get_password_hash(password: str) -> str:
|
||
return pwd_context.hash(password)
|
||
|
||
def create_access_token(data: dict, expires_delta: timedelta) -> str:
|
||
to_encode = data.copy()
|
||
expire = datetime.utcnow() + expires_delta
|
||
to_encode.update({"exp": expire})
|
||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||
return encoded_jwt
|
||
```
|
||
|
||
**Context**: ~11,750 tokens
|
||
|
||
### Stage 3: Database Schema (Minutes 10-15)
|
||
|
||
**Skill activates**: database-design (~3,500 tokens)
|
||
|
||
```python
|
||
# Agent designs schema following database-design skill
|
||
class User(Base):
|
||
__tablename__ = "users"
|
||
|
||
id = Column(Integer, primary_key=True, index=True)
|
||
email = Column(String, unique=True, index=True, nullable=False)
|
||
hashed_password = Column(String, nullable=False)
|
||
is_active = Column(Boolean, default=True)
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
|
||
__table_args__ = (
|
||
Index("idx_email_active", "email", "is_active"),
|
||
)
|
||
```
|
||
|
||
**Context**: ~15,250 tokens
|
||
|
||
### Stage 4: Code Style (Minutes 15-20)
|
||
|
||
**Skill already loaded**: python-standards (loaded at startup due to agent reference)
|
||
|
||
```python
|
||
# Agent applies python-standards throughout implementation
|
||
from typing import Optional, Dict, Any
|
||
from datetime import datetime, timedelta
|
||
|
||
def authenticate_user(
|
||
db: Session, email: str, password: str
|
||
) -> Optional[User]:
|
||
"""Authenticate user with email and password.
|
||
|
||
Args:
|
||
db: Database session
|
||
email: User email address
|
||
password: Plain text password
|
||
|
||
Returns:
|
||
User object if authentication successful, None otherwise
|
||
"""
|
||
user = db.query(User).filter(User.email == email).first()
|
||
if not user or not verify_password(password, user.hashed_password):
|
||
return None
|
||
return user
|
||
```
|
||
|
||
**Context**: ~15,250 tokens (skill already loaded)
|
||
|
||
### Stage 5: Testing (Minutes 20-30)
|
||
|
||
**Skill activates**: testing-guide (~5,000 tokens)
|
||
|
||
```python
|
||
# Agent writes tests following testing-guide skill
|
||
import pytest
|
||
from fastapi.testclient import TestClient
|
||
|
||
def test_login_success(client: TestClient, test_user):
|
||
"""Test successful user login returns access token."""
|
||
response = client.post(
|
||
"/auth/login",
|
||
json={"email": test_user.email, "password": "testpass123"}
|
||
)
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert "access_token" in data
|
||
assert "refresh_token" in data
|
||
assert data["token_type"] == "bearer"
|
||
|
||
def test_login_invalid_password(client: TestClient, test_user):
|
||
"""Test login with invalid password returns 401."""
|
||
response = client.post(
|
||
"/auth/login",
|
||
json={"email": test_user.email, "password": "wrongpassword"}
|
||
)
|
||
assert response.status_code == 401
|
||
assert response.json()["detail"] == "Incorrect email or password"
|
||
|
||
def test_access_protected_endpoint_without_token(client: TestClient):
|
||
"""Test accessing protected endpoint without token returns 401."""
|
||
response = client.get("/auth/me")
|
||
assert response.status_code == 401
|
||
```
|
||
|
||
**Context**: ~20,250 tokens
|
||
|
||
### Stage 6: Documentation (Minutes 30-35)
|
||
|
||
**Skill activates**: documentation-guide (~4,000 tokens)
|
||
|
||
```python
|
||
# Agent adds API documentation following documentation-guide skill
|
||
@app.post(
|
||
"/auth/login",
|
||
response_model=TokenResponse,
|
||
summary="User login",
|
||
description="""
|
||
Authenticate user with email and password.
|
||
|
||
Returns access token (15-minute expiry) and refresh token (7-day expiry).
|
||
|
||
**Authentication**: None required
|
||
|
||
**Rate limiting**: 10 requests per minute per IP
|
||
|
||
**Example request**:
|
||
```json
|
||
{
|
||
"email": "user@example.com",
|
||
"password": "securepassword123"
|
||
}
|
||
```
|
||
|
||
**Example response**:
|
||
```json
|
||
{
|
||
"access_token": "eyJhbGc...",
|
||
"refresh_token": "eyJhbGc...",
|
||
"token_type": "bearer"
|
||
}
|
||
```
|
||
"""
|
||
)
|
||
async def login(credentials: LoginRequest) -> TokenResponse:
|
||
pass
|
||
```
|
||
|
||
**Context**: ~24,250 tokens
|
||
|
||
### Stage 7: Observability (Minutes 35-40)
|
||
|
||
**Skill activates**: observability (~3,000 tokens)
|
||
|
||
```python
|
||
# Agent adds logging following observability skill
|
||
import logging
|
||
from opentelemetry import trace
|
||
|
||
tracer = trace.get_tracer(__name__)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
@app.post("/auth/login")
|
||
async def login(credentials: LoginRequest) -> TokenResponse:
|
||
with tracer.start_as_current_span("auth.login") as span:
|
||
span.set_attribute("user.email", credentials.email)
|
||
|
||
user = authenticate_user(db, credentials.email, credentials.password)
|
||
|
||
if not user:
|
||
logger.warning(
|
||
"Failed login attempt",
|
||
extra={"email": credentials.email, "ip": request.client.host}
|
||
)
|
||
raise HTTPException(status_code=401, detail="Incorrect email or password")
|
||
|
||
logger.info(
|
||
"Successful login",
|
||
extra={"user_id": user.id, "ip": request.client.host}
|
||
)
|
||
|
||
return create_tokens(user)
|
||
```
|
||
|
||
**Context**: ~27,250 tokens
|
||
|
||
## Total Token Usage
|
||
|
||
### Without Progressive Disclosure
|
||
|
||
If all 7 skills loaded upfront:
|
||
```
|
||
Agent prompt: 500 tokens
|
||
+ 7 skills × 5,000 tokens: 35,000 tokens
|
||
+ Task: 200 tokens
|
||
= 35,700 tokens before work starts!
|
||
```
|
||
|
||
### With Progressive Disclosure
|
||
|
||
Skills load as needed throughout implementation:
|
||
```
|
||
Startup: 1,750 tokens
|
||
+ Stage 1 (api-design): +4,000 tokens
|
||
+ Stage 2 (security-patterns): +6,000 tokens
|
||
+ Stage 3 (database-design): +3,500 tokens
|
||
+ Stage 5 (testing-guide): +5,000 tokens
|
||
+ Stage 6 (documentation-guide): +4,000 tokens
|
||
+ Stage 7 (observability): +3,000 tokens
|
||
= ~27,250 tokens total
|
||
|
||
Savings: 8,450 tokens (24% reduction)
|
||
```
|
||
|
||
## Key Observations
|
||
|
||
### Skill Coordination
|
||
|
||
Skills work together naturally:
|
||
- **api-design** provides endpoint structure
|
||
- **security-patterns** provides authentication implementation
|
||
- **database-design** provides schema
|
||
- **python-standards** ensures code quality throughout
|
||
- **testing-guide** ensures comprehensive testing
|
||
- **documentation-guide** ensures clear API docs
|
||
- **observability** ensures production monitoring
|
||
|
||
### No Conflicts
|
||
|
||
Skills complement each other:
|
||
- Each covers different domain
|
||
- No contradictory guidance
|
||
- Natural layering (design → implement → test → document)
|
||
|
||
### Efficient Loading
|
||
|
||
Progressive disclosure loads skills just-in-time:
|
||
- Not all at once (would exceed context)
|
||
- Not too late (available when needed)
|
||
- Automatic (agent doesn't manage loading)
|
||
|
||
## Summary
|
||
|
||
This example demonstrates:
|
||
- **7 skills working together** for complex task
|
||
- **Progressive loading** keeps context efficient
|
||
- **No conflicts** between skills
|
||
- **24% token savings** vs loading all upfront
|
||
- **Natural workflow** through implementation stages
|
||
|
||
**Key takeaway**: Trust progressive disclosure. Reference all relevant skills, let the system load them efficiently as needed.
|