diff --git a/FRONTEND_DEV_MODE_FEATURE.md b/FRONTEND_DEV_MODE_FEATURE.md new file mode 100644 index 000000000..035c2385f --- /dev/null +++ b/FRONTEND_DEV_MODE_FEATURE.md @@ -0,0 +1,183 @@ +# ADK FastAPI Frontend Development Mode + +## Overview + +The `--frontend-dev-mode` option is a new CLI flag for ADK FastAPI servers that significantly enhances the development experience when building frontend applications that connect to ADK agents. This feature automatically configures common development settings and provides additional debugging capabilities. + +## Features + +### 1. Automatic CORS Configuration +When `--frontend-dev-mode` is enabled, the server automatically adds CORS origins for common frontend development servers: + +- `http://localhost:3000` - React, Next.js default +- `http://localhost:3001` - React alternative port +- `http://localhost:4200` - Angular CLI default +- `http://localhost:5173` - Vite default +- `http://localhost:8080` - Vue CLI default +- Plus `127.0.0.1` variants of all above + +These are added in addition to any origins specified via `--allow_origins`. + +### 2. Enhanced Error Responses +In frontend dev mode, error responses include detailed debugging information: + +```json +{ + "error": "Session not found", + "type": "HTTPException", + "traceback": "Traceback (most recent call last):\n File \"...\", line 123...", + "request_info": { + "method": "GET", + "url": "http://localhost:8000/apps/my-app/users/user123/sessions/session456", + "headers": {...} + } +} +``` + +### 3. Development Headers +All responses include helpful headers for frontend development: + +- `X-Frontend-Dev-Mode: true` - Indicates dev mode is active +- `X-Server-Time: ` - Server timestamp for debugging +- Enhanced CORS headers for preflight requests + +### 4. Development Info Endpoint +A new `/dev-info` endpoint provides configuration information: + +```json +{ + "frontend_dev_mode": true, + "cors_origins": ["http://localhost:3000", "..."], + "server_time": 1234567890, + "features": { + "detailed_errors": true, + "dev_headers": true, + "auto_cors": true + } +} +``` + +## Usage + +### Basic Usage + +```bash +# Enable frontend dev mode with web UI +adk web --frontend-dev-mode /path/to/agents + +# Enable frontend dev mode with API server only +adk api_server --frontend-dev-mode --port 8000 /path/to/agents +``` + +### Combined with Custom CORS Origins + +```bash +# Add custom origins in addition to auto-configured dev origins +adk web --frontend-dev-mode --allow_origins https://myapp.com --allow_origins https://staging.myapp.com /path/to/agents +``` + +### Production vs Development + +```bash +# Development - with all dev features enabled +adk web --frontend-dev-mode --reload /path/to/agents + +# Production - clean configuration without dev features +adk web --allow_origins https://myapp.com --no-reload /path/to/agents +``` + +## Implementation Details + +### Files Modified + +1. **`src/google/adk/cli/cli_tools_click.py`** + - Added `--frontend-dev-mode` flag to `fast_api_common_options()` + - Updated `cli_web()` and `cli_api_server()` functions to accept and pass the parameter + +2. **`src/google/adk/cli/fast_api.py`** + - Modified `get_fast_api_app()` to accept `frontend_dev_mode` parameter + - Added automatic CORS origin configuration logic + - Implemented development middleware for enhanced error handling and headers + - Added `/dev-info` endpoint + +3. **`tests/unittests/cli/test_fast_api.py`** + - Added comprehensive tests for the new functionality + +### Key Implementation Features + +- **Backwards Compatible**: The feature is opt-in via CLI flag +- **Additive CORS**: Combines user-specified origins with auto-configured dev origins +- **Conditional Middleware**: Development features only active when flag is enabled +- **Error Safety**: Enhanced error handling doesn't break existing error responses + +## Benefits for Frontend Developers + +1. **Zero Configuration CORS**: No need to manually configure CORS for common dev servers +2. **Better Debugging**: Detailed error responses help identify issues quickly +3. **Development Visibility**: Headers and endpoints provide insight into server state +4. **Faster Iteration**: Automatic reload and dev-friendly settings speed up development + +## Security Considerations + +- **Development Only**: This feature should only be used in development environments +- **Detailed Errors**: Error responses include sensitive debugging information +- **Permissive CORS**: Automatically allows common localhost origins +- **Production Safety**: Feature is disabled by default and requires explicit opt-in + +## Frontend Integration Examples + +### React/Next.js (localhost:3000) +```javascript +// Automatically allowed when --frontend-dev-mode is enabled +const response = await fetch('http://localhost:8000/apps/my-app/users/user123/sessions'); +``` + +### Angular (localhost:4200) +```typescript +// Automatically allowed when --frontend-dev-mode is enabled +this.http.get('http://localhost:8000/dev-info').subscribe(info => { + console.log('Server dev mode:', info.frontend_dev_mode); +}); +``` + +### Vue/Vite (localhost:5173) +```javascript +// Check if server is in dev mode +const devInfo = await fetch('http://localhost:8000/dev-info').then(r => r.json()); +if (devInfo.frontend_dev_mode) { + console.log('Server is in development mode'); +} +``` + +## Migration Guide + +### Existing Projects +No changes required for existing projects. The feature is opt-in and backwards compatible. + +### New Projects +Consider using `--frontend-dev-mode` during development: + +```bash +# Old way - manual CORS configuration +adk web --allow_origins http://localhost:3000 /path/to/agents + +# New way - automatic dev configuration +adk web --frontend-dev-mode /path/to/agents +``` + +## Troubleshooting + +### CORS Issues +- Verify `--frontend-dev-mode` is enabled +- Check `/dev-info` endpoint to see configured origins +- Ensure your frontend dev server is using a supported port + +### Missing Debug Information +- Confirm `--frontend-dev-mode` flag is set +- Check response headers for `X-Frontend-Dev-Mode: true` +- Verify error responses include `traceback` field + +### Performance Concerns +- Development middleware adds minimal overhead +- Use `--no-reload` in production environments +- Disable `--frontend-dev-mode` for production deployments diff --git a/src/google/adk/cli/cli_tools_click.py b/src/google/adk/cli/cli_tools_click.py index 49ecee482..89c158790 100644 --- a/src/google/adk/cli/cli_tools_click.py +++ b/src/google/adk/cli/cli_tools_click.py @@ -514,6 +514,17 @@ def decorator(func): help="Optional. Any additional origins to allow for CORS.", multiple=True, ) + @click.option( + "--frontend-dev-mode", + is_flag=True, + show_default=True, + default=False, + help=( + "Optional. Enable frontend development mode. Automatically configures " + "CORS for common frontend dev ports (3000, 3001, 4200, 5173, 8080), " + "enables detailed error responses, and sets up proxy-friendly headers." + ), + ) @click.option( "--log_level", type=LOG_LEVELS, @@ -566,6 +577,7 @@ def cli_web( agents_dir: str, log_level: str = "INFO", allow_origins: Optional[list[str]] = None, + frontend_dev_mode: bool = False, host: str = "127.0.0.1", port: int = 8000, trace_to_cloud: bool = False, @@ -617,6 +629,7 @@ async def _lifespan(app: FastAPI): artifact_service_uri=artifact_service_uri, memory_service_uri=memory_service_uri, allow_origins=allow_origins, + frontend_dev_mode=frontend_dev_mode, web=True, trace_to_cloud=trace_to_cloud, lifespan=_lifespan, @@ -656,6 +669,7 @@ def cli_api_server( agents_dir: str, log_level: str = "INFO", allow_origins: Optional[list[str]] = None, + frontend_dev_mode: bool = False, host: str = "127.0.0.1", port: int = 8000, trace_to_cloud: bool = False, @@ -686,6 +700,7 @@ def cli_api_server( artifact_service_uri=artifact_service_uri, memory_service_uri=memory_service_uri, allow_origins=allow_origins, + frontend_dev_mode=frontend_dev_mode, web=False, trace_to_cloud=trace_to_cloud, ), diff --git a/src/google/adk/cli/fast_api.py b/src/google/adk/cli/fast_api.py index 46e008655..1916e0a76 100644 --- a/src/google/adk/cli/fast_api.py +++ b/src/google/adk/cli/fast_api.py @@ -199,6 +199,7 @@ def get_fast_api_app( artifact_service_uri: Optional[str] = None, memory_service_uri: Optional[str] = None, allow_origins: Optional[list[str]] = None, + frontend_dev_mode: bool = False, web: bool, trace_to_cloud: bool = False, lifespan: Optional[Lifespan[FastAPI]] = None, @@ -245,15 +246,77 @@ async def internal_lifespan(app: FastAPI): # Run the FastAPI server. app = FastAPI(lifespan=internal_lifespan) - if allow_origins: + # Configure CORS based on frontend development mode and explicit origins + cors_origins = list(allow_origins) if allow_origins else [] + + if frontend_dev_mode: + # Add common frontend development server ports + dev_origins = [ + "http://localhost:3000", # React, Next.js default + "http://localhost:3001", # React alternative + "http://localhost:4200", # Angular default + "http://localhost:5173", # Vite default + "http://localhost:8080", # Vue CLI default + "http://127.0.0.1:3000", + "http://127.0.0.1:3001", + "http://127.0.0.1:4200", + "http://127.0.0.1:5173", + "http://127.0.0.1:8080", + ] + cors_origins.extend(dev_origins) + + # Remove duplicates while preserving order + cors_origins = list(dict.fromkeys(cors_origins)) + + if cors_origins: app.add_middleware( CORSMiddleware, - allow_origins=allow_origins, + allow_origins=cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) + # Add frontend development mode enhancements + if frontend_dev_mode: + from fastapi import Request, Response + from fastapi.responses import JSONResponse + import traceback + + @app.middleware("http") + async def frontend_dev_middleware(request: Request, call_next): + try: + response = await call_next(request) + + # Add helpful headers for frontend development + response.headers["X-Frontend-Dev-Mode"] = "true" + response.headers["X-Server-Time"] = str(int(time.time())) + + # Add CORS headers for preflight requests if not already handled + if request.method == "OPTIONS": + response.headers["Access-Control-Allow-Origin"] = "*" + response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS" + response.headers["Access-Control-Allow-Headers"] = "*" + + return response + except Exception as e: + # In development mode, provide detailed error information + error_detail = { + "error": str(e), + "type": type(e).__name__, + "traceback": traceback.format_exc() if frontend_dev_mode else None, + "request_info": { + "method": request.method, + "url": str(request.url), + "headers": dict(request.headers), + } + } + return JSONResponse( + status_code=500, + content=error_detail, + headers={"X-Frontend-Dev-Mode": "true"} + ) + runner_dict = {} eval_sets_manager = LocalEvalSetsManager(agents_dir=agents_dir) @@ -312,6 +375,20 @@ async def internal_lifespan(app: FastAPI): # initialize Agent Loader agent_loader = AgentLoader(agents_dir) + @app.get("/dev-info") + def get_dev_info() -> dict[str, Any]: + """Provides information about development mode settings for frontend developers.""" + return { + "frontend_dev_mode": frontend_dev_mode, + "cors_origins": cors_origins if cors_origins else [], + "server_time": int(time.time()), + "features": { + "detailed_errors": frontend_dev_mode, + "dev_headers": frontend_dev_mode, + "auto_cors": frontend_dev_mode, + } + } + @app.get("/list-apps") def list_apps() -> list[str]: base_path = Path.cwd() / agents_dir diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index 65c1eee3b..a5fd76490 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -759,5 +759,111 @@ def test_debug_trace(test_app): logger.info("Debug trace test completed successfully") +def test_dev_info_endpoint_without_frontend_dev_mode(test_app): + """Test the /dev-info endpoint when frontend dev mode is disabled.""" + response = test_app.get("/dev-info") + + assert response.status_code == 200 + data = response.json() + + # Verify the structure and default values + assert "frontend_dev_mode" in data + assert "cors_origins" in data + assert "server_time" in data + assert "features" in data + + # Should be False since test_app doesn't enable frontend_dev_mode + assert data["frontend_dev_mode"] is False + assert data["features"]["detailed_errors"] is False + assert data["features"]["dev_headers"] is False + assert data["features"]["auto_cors"] is False + + logger.info("Dev info endpoint test completed successfully") + + +@pytest.fixture +def test_app_with_frontend_dev_mode( + mock_session_service, + mock_artifact_service, + mock_memory_service, + mock_agent_loader, + mock_eval_sets_manager, + mock_eval_set_results_manager, +): + """Create a TestClient for the FastAPI app with frontend dev mode enabled.""" + with patch( + "google.adk.cli.utils.agent_loader.AgentLoader", return_value=mock_agent_loader + ), patch( + "google.adk.cli.fast_api.InMemorySessionService", + return_value=mock_session_service, + ), patch( + "google.adk.cli.fast_api.InMemoryArtifactService", + return_value=mock_artifact_service, + ), patch( + "google.adk.cli.fast_api.InMemoryMemoryService", + return_value=mock_memory_service, + ), patch( + "google.adk.cli.fast_api.LocalEvalSetsManager", + return_value=mock_eval_sets_manager, + ), patch( + "google.adk.cli.fast_api.LocalEvalSetResultsManager", + return_value=mock_eval_set_results_manager, + ), patch( + "google.adk.cli.cli_eval.run_evals", + new=mock_run_evals_for_fast_api, + ): + # Get the FastAPI app with frontend dev mode enabled + app = get_fast_api_app( + agents_dir=".", + web=True, + session_service_uri="", + artifact_service_uri="", + memory_service_uri="", + allow_origins=["http://example.com:3000"], + frontend_dev_mode=True, # Enable frontend dev mode + ) + + # Create a TestClient + client = TestClient(app) + return client + + +def test_dev_info_endpoint_with_frontend_dev_mode(test_app_with_frontend_dev_mode): + """Test the /dev-info endpoint when frontend dev mode is enabled.""" + response = test_app_with_frontend_dev_mode.get("/dev-info") + + assert response.status_code == 200 + data = response.json() + + # Verify frontend dev mode is enabled + assert data["frontend_dev_mode"] is True + assert data["features"]["detailed_errors"] is True + assert data["features"]["dev_headers"] is True + assert data["features"]["auto_cors"] is True + + # Verify CORS origins include both explicit and auto-added dev origins + cors_origins = data["cors_origins"] + assert "http://example.com:3000" in cors_origins # Explicit origin + assert "http://localhost:3000" in cors_origins # Auto-added dev origin + assert "http://localhost:4200" in cors_origins # Auto-added dev origin + assert "http://localhost:5173" in cors_origins # Auto-added dev origin + + logger.info("Frontend dev mode endpoint test completed successfully") + + +def test_frontend_dev_mode_headers(test_app_with_frontend_dev_mode): + """Test that frontend dev mode adds helpful headers to responses.""" + response = test_app_with_frontend_dev_mode.get("/list-apps") + + assert response.status_code == 200 + + # Verify dev mode headers are present + assert "X-Frontend-Dev-Mode" in response.headers + assert response.headers["X-Frontend-Dev-Mode"] == "true" + assert "X-Server-Time" in response.headers + + logger.info("Frontend dev mode headers test completed successfully") + + if __name__ == "__main__": pytest.main(["-xvs", __file__]) diff --git a/tests/unittests/conftest.py b/tests/unittests/conftest.py index 2b93226db..335f3813a 100644 --- a/tests/unittests/conftest.py +++ b/tests/unittests/conftest.py @@ -17,7 +17,7 @@ from pytest import fixture from pytest import FixtureRequest from pytest import hookimpl -from pytest import Metafunc +from _pytest.python import Metafunc _ENV_VARS = { 'GOOGLE_API_KEY': 'fake_google_api_key',