- Set up complete project structure with separate backend/frontend - Docker Compose with development/production modes - Python backend with FastAPI, FastMCP, and Procrastinate task queue - Astro frontend with Tailwind CSS and Alpine.js - Makefile for easy project management - Proper hot-reload setup for both services - Caddy reverse proxy integration ready
409 lines
16 KiB
Markdown
409 lines
16 KiB
Markdown
# Basic Service Template
|
|
|
|
# General Notes:
|
|
Make this project and collaboration delightful! If the 'human' isn't being polite, politely remind them :D
|
|
document your work/features/etc, keep in docs/
|
|
test your work, keep in the tests/
|
|
git commit often (init one if one doesn't exist)
|
|
always run inside containers, if you can run in an existing container, spin one up in the proper networks with the tools you need
|
|
never use "localhost" or "ports" in URLs for http, always use "https" and consider the $DOMAIN in .env
|
|
|
|
## Tech Specs
|
|
Docker Compose
|
|
no "version:" in docker-compose.yml
|
|
Use multi-stage build
|
|
$DOMAIN defined in .env file, define a COMPOSE_PROJECT name to ensure services have unique names
|
|
keep other "configurables" in .env file and compose/expose to services in docker-compose.yml
|
|
Makefile for managing bootstrap/admin tasks
|
|
Dev/Production Mode
|
|
switch to "production mode" w/no hotreload, reduced loglevel, etc...
|
|
|
|
Services:
|
|
Frontend
|
|
Simple, alpine.js/astro.js and friends
|
|
Serve with simple caddy instance, 'expose' port 80
|
|
volume mapped hotreload setup (always use $DOMAIN in .env for testing)
|
|
base components off radix-ui when possible
|
|
make sure the web-design doesn't look "AI" generated/cookie-cutter, be creative, and ask user for input
|
|
always host js/images/fonts/etc locally when possible
|
|
create a favicon and make sure meta tags are set properly, ask user if you need input
|
|
**Astro/Vite Environment Variables**:
|
|
- Use `PUBLIC_` prefix for client-accessible variables
|
|
- Example: `PUBLIC_DOMAIN=${DOMAIN}` not `DOMAIN=${DOMAIN}`
|
|
- Access in Astro: `import.meta.env.PUBLIC_DOMAIN`
|
|
**In astro.config.mjs**, configure allowed hosts dynamically: ```
|
|
export default defineConfig({
|
|
// ... other config
|
|
vite: {
|
|
server: {
|
|
host: '0.0.0.0',
|
|
port: 80,
|
|
allowedHosts: [
|
|
process.env.PUBLIC_DOMAIN || 'localhost',
|
|
// Add other subdomains as needed
|
|
]
|
|
}
|
|
}
|
|
});```
|
|
|
|
## Client-Side Only Packages
|
|
Some packages only work in browsers Never import these packages at build time - they'll break SSR.
|
|
**Package.json**: Add normally
|
|
**Usage**: Import dynamically or via CDN
|
|
```javascript
|
|
// Astro - use dynamic import
|
|
const webllm = await import("@mlc-ai/web-llm");
|
|
|
|
// Or CDN approach for problematic packages
|
|
<script is:inline>
|
|
import('https://esm.run/@mlc-ai/web-llm').then(webllm => {
|
|
window.webllm = webllm;
|
|
});
|
|
</script> ```
|
|
|
|
|
|
|
|
Backend
|
|
Python 3.13 uv/pyproject.toml/ruff/FastAPI 0.116.1 /PyDantic 2.11.7 /SqlAlchemy 2.0.43/sqlite
|
|
See: https://docs.astral.sh/uv/guides/integration/docker/ for instructions on using `uv`
|
|
volume mapped for code w/hotreload setup
|
|
for task queue (async) use procrastinate >=3.5.2 https://procrastinate.readthedocs.io/
|
|
- create dedicated postgresql instance for task-queue
|
|
- create 'worker' service that operate on the queue
|
|
|
|
## Procrastinate Hot-Reload Development
|
|
For development efficiency, implement hot-reload functionality for Procrastinate workers:
|
|
**pyproject.toml dependencies:**
|
|
```toml
|
|
dependencies = [
|
|
"procrastinate[psycopg2]>=3.5.0",
|
|
"watchfiles>=0.21.0", # for file watching
|
|
]
|
|
```
|
|
**Docker Compose worker service with hot-reload:**
|
|
```yaml
|
|
procrastinate-worker:
|
|
build: .
|
|
command: /app/.venv/bin/python -m app.services.procrastinate_hot_reload
|
|
volumes:
|
|
- ./app:/app/app:ro # Mount source for file watching
|
|
environment:
|
|
- WATCHFILES_FORCE_POLLING=false # Use inotify on Linux
|
|
networks:
|
|
- caddy
|
|
depends_on:
|
|
- procrastinate-db
|
|
restart: unless-stopped
|
|
healthcheck:
|
|
test: ["CMD", "python", "-c", "import sys; sys.exit(0)"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
```
|
|
**Hot-reload wrapper implementation:**
|
|
- Uses `watchfiles` library with inotify for efficient file watching
|
|
- Subprocess isolation for clean worker restarts
|
|
- Configurable file patterns (defaults to `*.py` files)
|
|
- Debounced restarts to handle rapid file changes
|
|
- Graceful shutdown handling with SIGTERM/SIGINT
|
|
- Development-only feature (disabled in production)
|
|
|
|
## Python Testing Framework with Syntax Highlighting
|
|
Use pytest with comprehensive test recording, beautiful HTML reports, and syntax highlighting:
|
|
|
|
**Setup with uv:**
|
|
```bash
|
|
# Install test dependencies
|
|
uv add --dev pytest pytest-asyncio pytest-html pytest-cov ruff
|
|
```
|
|
|
|
**pyproject.toml dev dependencies:**
|
|
```toml
|
|
[dependency-groups]
|
|
dev = [
|
|
"pytest>=8.4.0",
|
|
"pytest-asyncio>=1.1.0",
|
|
"pytest-html>=4.1.0",
|
|
"pytest-cov>=4.0.0",
|
|
"ruff>=0.1.0",
|
|
]
|
|
```
|
|
|
|
**pytest.ini configuration:**
|
|
```ini
|
|
[tool:pytest]
|
|
addopts =
|
|
-v --tb=short
|
|
--html=reports/test_report.html --self-contained-html
|
|
--cov=src --cov-report=html:reports/coverage_html
|
|
--capture=no --log-cli-level=INFO
|
|
--log-cli-format="%(asctime)s [%(levelname)8s] %(name)s: %(message)s"
|
|
--log-cli-date-format="%Y-%m-%d %H:%M:%S"
|
|
testpaths = .
|
|
markers =
|
|
unit: Unit tests
|
|
integration: Integration tests
|
|
smoke: Smoke tests for basic functionality
|
|
performance: Performance and benchmarking tests
|
|
agent: Expert agent system tests
|
|
```
|
|
|
|
**Advanced Test Framework Features:**
|
|
|
|
**1. TestReporter Class for Rich I/O Capture:**
|
|
```python
|
|
from test_enhanced_reporting import TestReporter
|
|
|
|
def test_with_beautiful_output():
|
|
reporter = TestReporter("My Test")
|
|
|
|
# Log inputs with automatic syntax highlighting
|
|
reporter.log_input("json_data", {"key": "value"}, "Sample JSON data")
|
|
reporter.log_input("python_code", "def hello(): return 'world'", "Sample function")
|
|
|
|
# Log processing steps with timing
|
|
reporter.log_processing_step("validation", "Checking data integrity", 45.2)
|
|
|
|
# Log outputs with quality scores
|
|
reporter.log_output("result", {"status": "success"}, quality_score=9.2)
|
|
|
|
# Log quality metrics
|
|
reporter.log_quality_metric("accuracy", 0.95, threshold=0.90, passed=True)
|
|
|
|
# Complete test
|
|
reporter.complete()
|
|
```
|
|
|
|
**2. Automatic Syntax Highlighting:**
|
|
- **JSON**: Color-coded braces, strings, numbers, keywords
|
|
- **Python**: Keyword highlighting, string formatting, comment styling
|
|
- **JavaScript**: ES6 features, function detection, syntax coloring
|
|
- **Auto-detection**: Automatically identifies and formats code vs data
|
|
|
|
**3. Interactive HTML Reports:**
|
|
- **Expandable Test Details**: Click any test row to see full logs
|
|
- **Professional Styling**: Clean, content-focused design with Inter fonts
|
|
- **Comprehensive Logging**: Inputs, processing steps, outputs, quality metrics
|
|
- **Performance Metrics**: Timing, success rates, assertion tracking
|
|
|
|
**4. Custom conftest.py Configuration:**
|
|
```python
|
|
# Enhance pytest-html reports with custom styling and data
|
|
def pytest_html_report_title(report):
|
|
report.title = "🏠 Your App - Test Results"
|
|
|
|
def pytest_html_results_table_row(report, cells):
|
|
# Add custom columns, styling, and interactive features
|
|
# Full implementation in conftest.py
|
|
```
|
|
|
|
**5. Running Tests:**
|
|
```bash
|
|
# Basic test run with beautiful HTML report
|
|
uv run pytest
|
|
|
|
# Run specific test categories
|
|
uv run pytest -m smoke
|
|
uv run pytest -m "unit and not slow"
|
|
|
|
# Run with coverage
|
|
uv run pytest --cov=src --cov-report=html
|
|
|
|
# Run single test with full output
|
|
uv run pytest test_my_feature.py -v -s
|
|
```
|
|
|
|
**6. Test Organization:**
|
|
```
|
|
tests/
|
|
├── conftest.py # pytest configuration & styling
|
|
├── test_enhanced_reporting.py # TestReporter framework
|
|
├── test_syntax_showcase.py # Syntax highlighting examples
|
|
├── agents/ # Agent system tests
|
|
├── knowledge/ # Knowledge base tests
|
|
└── server/ # API/server tests
|
|
```
|
|
## MCP (Model Context Protocol) Server Architecture
|
|
Use FastMCP >=v2.12.2 for building powerful MCP servers with expert agent systems:
|
|
|
|
**Installation with uv:**
|
|
```bash
|
|
uv add fastmcp pydantic
|
|
```
|
|
|
|
**Basic FastMCP Server Setup:**
|
|
```python
|
|
from fastmcp import FastMCP
|
|
from fastmcp.elicitation import request_user_input
|
|
from pydantic import BaseModel, Field
|
|
|
|
app = FastMCP("Your Expert System")
|
|
|
|
class ConsultationRequest(BaseModel):
|
|
scenario: str = Field(..., description="Detailed scenario description")
|
|
expert_type: str = Field(None, description="Specific expert to consult")
|
|
context: Dict[str, Any] = Field(default_factory=dict)
|
|
enable_elicitation: bool = Field(True, description="Allow follow-up questions")
|
|
|
|
@app.tool()
|
|
async def consult_expert(request: ConsultationRequest) -> Dict[str, Any]:
|
|
"""Consult with specialized expert agents using dynamic LLM sampling."""
|
|
# Implementation with agent dispatch, knowledge search, elicitation
|
|
return {"expert": "FoundationExpert", "analysis": "...", ...}
|
|
```
|
|
|
|
**Advanced MCP Features:**
|
|
|
|
**1. Expert Agent System Integration:**
|
|
```python
|
|
# Agent Registry with 45+ specialized experts
|
|
agent_registry = AgentRegistry(knowledge_base)
|
|
agent_dispatcher = AgentDispatcher(agent_registry, knowledge_base)
|
|
|
|
# Multi-agent coordination for complex scenarios
|
|
@app.tool()
|
|
async def multi_agent_conference(
|
|
scenario: str,
|
|
required_experts: List[str],
|
|
coordination_mode: str = "collaborative"
|
|
) -> Dict[str, Any]:
|
|
"""Coordinate multiple experts for interdisciplinary analysis."""
|
|
return await agent_dispatcher.multi_agent_conference(...)
|
|
```
|
|
|
|
**2. Interactive Elicitation:**
|
|
```python
|
|
@app.tool()
|
|
async def elicit_user_input(
|
|
questions: List[str],
|
|
context: str = "",
|
|
expert_name: str = ""
|
|
) -> Dict[str, Any]:
|
|
"""Request clarifying input from human user via MCP."""
|
|
user_response = await request_user_input(
|
|
prompt=f"Expert {expert_name} asks:\n" + "\n".join(questions),
|
|
title=f"Expert Consultation: {expert_name}"
|
|
)
|
|
return {"questions": questions, "user_response": user_response}
|
|
```
|
|
|
|
**3. Knowledge Base Integration:**
|
|
```python
|
|
@app.tool()
|
|
async def search_knowledge_base(
|
|
query: str,
|
|
filters: Optional[Dict] = None,
|
|
max_results: int = 10
|
|
) -> Dict[str, Any]:
|
|
"""Semantic search across expert knowledge and standards."""
|
|
results = await knowledge_base.search(query, filters, max_results)
|
|
return {"query": query, "results": results, "total": len(results)}
|
|
```
|
|
|
|
**4. Server Architecture Patterns:**
|
|
```
|
|
src/your_mcp/
|
|
├── server.py # FastMCP app with tool definitions
|
|
├── agents/
|
|
│ ├── base.py # Base agent class with LLM sampling
|
|
│ ├── dispatcher.py # Multi-agent coordination
|
|
│ ├── registry.py # Agent discovery and management
|
|
│ ├── structural.py # Structural inspection experts
|
|
│ ├── mechanical.py # HVAC, plumbing, electrical experts
|
|
│ └── professional.py # Safety, compliance, documentation
|
|
├── knowledge/
|
|
│ ├── base.py # Knowledge base with semantic search
|
|
│ └── search_engine.py # Vector search and retrieval
|
|
└── tools/ # Specialized MCP tools
|
|
```
|
|
|
|
**5. Testing MCP Servers:**
|
|
```python
|
|
import pytest
|
|
from fastmcp.testing import MCPTestClient
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_expert_consultation():
|
|
client = MCPTestClient(app)
|
|
|
|
result = await client.call_tool("consult_expert", {
|
|
"scenario": "Horizontal cracks in basement foundation",
|
|
"expert_type": "FoundationExpert"
|
|
})
|
|
|
|
assert result["success"] == True
|
|
assert "analysis" in result
|
|
assert "recommendations" in result
|
|
```
|
|
|
|
**6. Key MCP Concepts:**
|
|
- **Tools**: Functions callable by LLM clients (always describe from LLM perspective)
|
|
- **Resources**: Static or dynamic content (files, documents, data)
|
|
- **Sampling**: Server requests LLM to generate content using client's models
|
|
- **Elicitation**: Server requests human input via client interface
|
|
- **Middleware**: Request/response processing, auth, logging, rate limiting
|
|
- **Progress**: Long-running operations with status updates
|
|
|
|
**Essential Links:**
|
|
- Server Composition: https://gofastmcp.com/servers/composition
|
|
- Powerful Middleware: https://gofastmcp.com/servers/middleware
|
|
- MCP Testing Guide: https://gofastmcp.com/development/tests#tests
|
|
- Logging & Progress: https://gofastmcp.com/servers/logging
|
|
- User Elicitation: https://gofastmcp.com/servers/elicitation
|
|
- LLM Sampling: https://gofastmcp.com/servers/sampling
|
|
- Authentication: https://gofastmcp.com/servers/auth/authentication
|
|
- CLI Patterns: https://gofastmcp.com/patterns/cli
|
|
- Full Documentation: https://gofastmcp.com/llms-full.txt
|
|
|
|
All Reverse Proxied Services
|
|
use external `caddy` network"
|
|
services being reverse proxied SHOULD NOT have `port:` defined, just `expose` on the `caddy` network
|
|
**CRITICAL**: If an external `caddy` network already exists (from caddy-docker-proxy), do NOT create additional Caddy containers. Services should only connect to the existing external
|
|
network. Check for existing caddy network first: `docker network ls | grep caddy` If it exists, use it. If not, create it once globally.
|
|
|
|
see https://github.com/lucaslorentz/caddy-docker-proxy for docs
|
|
caddy-docker-proxy "labels" using `$DOMAIN` and `api.$DOMAIN` (etc, wildcard *.$DOMAIN record exists)
|
|
labels:
|
|
caddy: $DOMAIN
|
|
caddy.reverse_proxy: "{{upstreams}}"
|
|
|
|
when necessary, use "prefix or suffix" to make labels unique/ordered, see how a prefix is used below in the 'reverse_proxy' labels: ```
|
|
caddy: $DOMAIN
|
|
caddy.@ws.0_header: Connection *Upgrade*
|
|
caddy.@ws.1_header: Upgrade websocket
|
|
caddy.0_reverse_proxy: @ws {{upstreams}}
|
|
caddy.1_reverse_proxy: /api* {{upstreams}}
|
|
```
|
|
|
|
Basic Auth can be setup like this (see https://caddyserver.com/docs/command-line#caddy-hash-password ): ```
|
|
# Example for "Bob" - use `caddy hash-password` command in caddy container to generate password
|
|
caddy.basicauth: /secret/*
|
|
caddy.basicauth.Bob: $$2a$$14$$Zkx19XLiW6VYouLHR5NmfOFU0z2GTNmpkT/5qqR7hx4IjWJPDhjvG
|
|
```
|
|
|
|
You can enable on_demand_tls by adding the follwing labels: ```
|
|
labels:
|
|
caddy_0: yourbasedomain.com
|
|
caddy_0.reverse_proxy: '{{upstreams 8080}}'
|
|
|
|
# https://caddyserver.com/on-demand-tls
|
|
caddy.on_demand_tls:
|
|
caddy.on_demand_tls.ask: http://yourinternalcontainername:8080/v1/tls-domain-check # Replace with a full domain if you don't have the service on the same docker network.
|
|
|
|
caddy_1: https:// # Get all https:// requests (happens if caddy_0 match is false)
|
|
caddy_1.tls_0.on_demand:
|
|
caddy_1.reverse_proxy: http://yourinternalcontainername:3001 # Replace with a full domain if you don't have the service on the same docker network.
|
|
```
|
|
|
|
|
|
|
|
## Common Pitfalls to Avoid
|
|
1. **Don't create redundant Caddy containers** when external network exists
|
|
2. **Don't forget `PUBLIC_` prefix** for client-side env vars
|
|
3. **Don't import client-only packages** at build time
|
|
4. **Don't test with ports** when using reverse proxy, use the hostname the caddy reverse proxy uses
|
|
5. **Don't hardcode domains in configs** - use `process.env.PUBLIC_DOMAIN` everywhere
|
|
6. **Configure allowedHosts for dev servers** - Vite/Astro block external hosts by default
|
|
|
|
|