2024-04-21 16:46:27 -07:00
|
|
|
import uuid
|
2025-10-15 17:22:07 -07:00
|
|
|
from contextlib import asynccontextmanager
|
2024-04-21 16:46:27 -07:00
|
|
|
from datetime import datetime
|
2025-10-15 17:22:07 -07:00
|
|
|
from typing import Any, AsyncGenerator, Awaitable, Callable
|
2024-04-21 16:46:27 -07:00
|
|
|
|
|
|
|
|
import structlog
|
2024-08-12 19:36:24 +03:00
|
|
|
from fastapi import FastAPI, Response, status
|
2024-04-21 16:46:27 -07:00
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
2025-03-21 09:21:24 -07:00
|
|
|
from fastapi.openapi.utils import get_openapi
|
2024-04-21 16:46:27 -07:00
|
|
|
from fastapi.responses import JSONResponse
|
2024-06-12 12:39:05 -07:00
|
|
|
from pydantic import ValidationError
|
2024-04-21 16:46:27 -07:00
|
|
|
from starlette.requests import HTTPConnection, Request
|
|
|
|
|
from starlette_context.middleware import RawContextMiddleware
|
|
|
|
|
from starlette_context.plugins.base import Plugin
|
|
|
|
|
|
2024-12-02 15:01:22 -08:00
|
|
|
from skyvern.config import settings
|
2024-05-15 13:35:45 -07:00
|
|
|
from skyvern.exceptions import SkyvernHTTPException
|
2024-04-22 00:44:16 -07:00
|
|
|
from skyvern.forge import app as forge_app
|
2025-11-18 17:56:58 -07:00
|
|
|
from skyvern.forge.forge_app_initializer import start_forge_app
|
2025-06-17 23:34:39 -04:00
|
|
|
from skyvern.forge.request_logging import log_raw_request_middleware
|
2024-04-21 16:46:27 -07:00
|
|
|
from skyvern.forge.sdk.core import skyvern_context
|
|
|
|
|
from skyvern.forge.sdk.core.skyvern_context import SkyvernContext
|
2024-06-16 19:42:20 -07:00
|
|
|
from skyvern.forge.sdk.db.exceptions import NotFoundError
|
2025-10-13 07:55:59 -07:00
|
|
|
from skyvern.forge.sdk.routes import internal_auth
|
2025-03-30 23:57:54 -07:00
|
|
|
from skyvern.forge.sdk.routes.routers import base_router, legacy_base_router, legacy_v2_router
|
2026-02-18 23:10:00 +08:00
|
|
|
from skyvern.services.cleanup_service import start_cleanup_scheduler, stop_cleanup_scheduler
|
2024-04-21 16:46:27 -07:00
|
|
|
|
2026-02-04 17:54:07 -08:00
|
|
|
try:
|
|
|
|
|
from cloud.observability.otel_setup import OTELSetup
|
|
|
|
|
except ImportError:
|
|
|
|
|
OTELSetup = None # type: ignore[assignment,misc]
|
|
|
|
|
|
2024-04-21 16:46:27 -07:00
|
|
|
LOG = structlog.get_logger()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ExecutionDatePlugin(Plugin):
|
|
|
|
|
key = "execution_date"
|
|
|
|
|
|
|
|
|
|
async def process_request(self, request: Request | HTTPConnection) -> datetime:
|
|
|
|
|
return datetime.now()
|
|
|
|
|
|
|
|
|
|
|
2025-11-18 17:56:58 -07:00
|
|
|
def custom_openapi(app: FastAPI) -> dict:
|
2025-03-21 09:21:24 -07:00
|
|
|
if app.openapi_schema:
|
|
|
|
|
return app.openapi_schema
|
|
|
|
|
openapi_schema = get_openapi(
|
|
|
|
|
title="Skyvern API",
|
|
|
|
|
version="1.0.0",
|
|
|
|
|
description="API for Skyvern",
|
|
|
|
|
routes=app.routes,
|
|
|
|
|
)
|
2025-03-21 13:45:26 -07:00
|
|
|
openapi_schema["servers"] = [
|
2025-10-31 10:40:33 -06:00
|
|
|
{"url": "https://api.skyvern.com", "x-fern-server-name": "Cloud"},
|
2025-03-21 13:45:26 -07:00
|
|
|
{"url": "https://api-staging.skyvern.com", "x-fern-server-name": "Staging"},
|
2025-10-31 10:40:33 -06:00
|
|
|
{"url": "http://localhost:8000", "x-fern-server-name": "Local"},
|
2025-03-21 13:45:26 -07:00
|
|
|
]
|
2025-03-21 09:21:24 -07:00
|
|
|
app.openapi_schema = openapi_schema
|
|
|
|
|
return app.openapi_schema
|
|
|
|
|
|
|
|
|
|
|
2025-10-15 17:22:07 -07:00
|
|
|
@asynccontextmanager
|
2026-02-04 17:54:07 -08:00
|
|
|
async def lifespan(fastapi_app: FastAPI) -> AsyncGenerator[None, Any]:
|
2025-10-15 17:22:07 -07:00
|
|
|
"""Lifespan context manager for FastAPI app startup and shutdown."""
|
2025-11-18 17:56:58 -07:00
|
|
|
|
2025-10-15 17:22:07 -07:00
|
|
|
LOG.info("Server started")
|
2025-10-15 22:39:13 -07:00
|
|
|
if forge_app.api_app_startup_event:
|
|
|
|
|
LOG.info("Calling api app startup event")
|
|
|
|
|
try:
|
2026-02-04 17:54:07 -08:00
|
|
|
await forge_app.api_app_startup_event(fastapi_app)
|
2025-10-15 22:39:13 -07:00
|
|
|
except Exception:
|
|
|
|
|
LOG.exception("Failed to execute api app startup event")
|
2026-02-18 23:10:00 +08:00
|
|
|
|
|
|
|
|
# Start cleanup scheduler if enabled
|
|
|
|
|
cleanup_task = start_cleanup_scheduler()
|
|
|
|
|
if cleanup_task:
|
|
|
|
|
LOG.info("Cleanup scheduler started")
|
|
|
|
|
|
2025-10-15 17:22:07 -07:00
|
|
|
yield
|
2026-02-18 23:10:00 +08:00
|
|
|
|
|
|
|
|
# Stop cleanup scheduler
|
|
|
|
|
await stop_cleanup_scheduler()
|
|
|
|
|
|
2025-10-15 22:39:13 -07:00
|
|
|
if forge_app.api_app_shutdown_event:
|
|
|
|
|
LOG.info("Calling api app shutdown event")
|
|
|
|
|
try:
|
|
|
|
|
await forge_app.api_app_shutdown_event()
|
|
|
|
|
except Exception:
|
|
|
|
|
LOG.exception("Failed to execute api app shutdown event")
|
2025-10-15 17:22:07 -07:00
|
|
|
LOG.info("Server shutting down")
|
|
|
|
|
|
|
|
|
|
|
2025-11-18 17:56:58 -07:00
|
|
|
def create_api_app() -> FastAPI:
|
2024-04-21 16:46:27 -07:00
|
|
|
"""
|
|
|
|
|
Start the agent server.
|
|
|
|
|
"""
|
2026-02-04 17:54:07 -08:00
|
|
|
# CRITICAL: Initialize OTEL FIRST, before any other code runs
|
|
|
|
|
# This must happen before start_forge_app() because that function
|
|
|
|
|
# creates database connections. If we don't instrument the libraries
|
|
|
|
|
# first, the DB spans won't be children of the HTTP request spans.
|
|
|
|
|
if settings.OTEL_ENABLED and OTELSetup is not None:
|
|
|
|
|
try:
|
|
|
|
|
otel = OTELSetup.get_instance()
|
|
|
|
|
otel.initialize_tracer_provider()
|
|
|
|
|
LOG.info("OTEL tracer provider initialized before forge app creation")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
LOG.warning("Failed to initialize OTEL tracer provider early", error=str(e))
|
2024-04-21 16:46:27 -07:00
|
|
|
|
2025-11-18 17:56:58 -07:00
|
|
|
forge_app_instance = start_forge_app()
|
|
|
|
|
|
|
|
|
|
fastapi_app = FastAPI(lifespan=lifespan)
|
2024-04-21 16:46:27 -07:00
|
|
|
|
|
|
|
|
# Add CORS middleware
|
2025-11-18 17:56:58 -07:00
|
|
|
fastapi_app.add_middleware(
|
2024-04-21 16:46:27 -07:00
|
|
|
CORSMiddleware,
|
2024-12-02 15:01:22 -08:00
|
|
|
allow_origins=settings.ALLOWED_ORIGINS,
|
2024-04-21 16:46:27 -07:00
|
|
|
allow_credentials=True,
|
|
|
|
|
allow_methods=["*"],
|
|
|
|
|
allow_headers=["*"],
|
|
|
|
|
)
|
|
|
|
|
|
2025-11-18 17:56:58 -07:00
|
|
|
fastapi_app.include_router(base_router, prefix="/v1")
|
|
|
|
|
fastapi_app.include_router(legacy_base_router, prefix="/api/v1")
|
|
|
|
|
fastapi_app.include_router(legacy_v2_router, prefix="/api/v2")
|
2025-10-13 07:55:59 -07:00
|
|
|
|
|
|
|
|
# local dev endpoints
|
|
|
|
|
if settings.ENV == "local":
|
2025-11-18 17:56:58 -07:00
|
|
|
fastapi_app.include_router(internal_auth.router, prefix="/v1")
|
|
|
|
|
fastapi_app.include_router(internal_auth.router, prefix="/api/v1")
|
|
|
|
|
fastapi_app.include_router(internal_auth.router, prefix="/api/v2")
|
2025-10-13 07:55:59 -07:00
|
|
|
|
2025-11-18 17:56:58 -07:00
|
|
|
fastapi_app.openapi = lambda: custom_openapi(fastapi_app)
|
2024-04-21 16:46:27 -07:00
|
|
|
|
2025-11-18 17:56:58 -07:00
|
|
|
fastapi_app.add_middleware(
|
2024-04-21 16:46:27 -07:00
|
|
|
RawContextMiddleware,
|
|
|
|
|
plugins=(
|
|
|
|
|
# TODO (suchintan): We should set these up
|
|
|
|
|
ExecutionDatePlugin(),
|
|
|
|
|
# RequestIdPlugin(),
|
|
|
|
|
# UserAgentPlugin(),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
2025-11-18 17:56:58 -07:00
|
|
|
@fastapi_app.exception_handler(NotFoundError)
|
2024-06-16 19:42:20 -07:00
|
|
|
async def handle_not_found_error(request: Request, exc: NotFoundError) -> Response:
|
|
|
|
|
return Response(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
2025-11-18 17:56:58 -07:00
|
|
|
@fastapi_app.exception_handler(SkyvernHTTPException)
|
2024-05-15 13:35:45 -07:00
|
|
|
async def handle_skyvern_http_exception(request: Request, exc: SkyvernHTTPException) -> JSONResponse:
|
|
|
|
|
return JSONResponse(status_code=exc.status_code, content={"detail": exc.message})
|
|
|
|
|
|
2025-11-18 17:56:58 -07:00
|
|
|
@fastapi_app.exception_handler(ValidationError)
|
2024-06-12 12:39:05 -07:00
|
|
|
async def handle_pydantic_validation_error(request: Request, exc: ValidationError) -> JSONResponse:
|
2025-03-21 09:21:24 -07:00
|
|
|
return JSONResponse(
|
|
|
|
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
|
|
|
content={"detail": str(exc)},
|
|
|
|
|
)
|
2024-06-12 12:39:05 -07:00
|
|
|
|
2025-11-18 17:56:58 -07:00
|
|
|
@fastapi_app.exception_handler(Exception)
|
2024-04-21 16:46:27 -07:00
|
|
|
async def unexpected_exception(request: Request, exc: Exception) -> JSONResponse:
|
|
|
|
|
LOG.exception("Unexpected error in agent server.", exc_info=exc)
|
|
|
|
|
return JSONResponse(status_code=500, content={"error": f"Unexpected error: {exc}"})
|
|
|
|
|
|
2025-11-18 17:56:58 -07:00
|
|
|
@fastapi_app.middleware("http")
|
2024-04-21 16:46:27 -07:00
|
|
|
async def request_middleware(request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
2024-05-07 11:23:17 -04:00
|
|
|
curr_ctx = skyvern_context.current()
|
|
|
|
|
if not curr_ctx:
|
|
|
|
|
request_id = str(uuid.uuid4())
|
|
|
|
|
skyvern_context.set(SkyvernContext(request_id=request_id))
|
|
|
|
|
elif not curr_ctx.request_id:
|
|
|
|
|
curr_ctx.request_id = str(uuid.uuid4())
|
2024-04-21 16:46:27 -07:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
return await call_next(request)
|
|
|
|
|
finally:
|
|
|
|
|
skyvern_context.reset()
|
|
|
|
|
|
2025-11-18 17:56:58 -07:00
|
|
|
@fastapi_app.middleware("http")
|
2025-06-17 23:34:39 -04:00
|
|
|
async def raw_request_logging(request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
|
|
|
|
return await log_raw_request_middleware(request, call_next)
|
|
|
|
|
|
2025-11-18 17:56:58 -07:00
|
|
|
if forge_app_instance.setup_api_app:
|
|
|
|
|
forge_app_instance.setup_api_app(fastapi_app)
|
2024-04-21 16:46:27 -07:00
|
|
|
|
2025-11-18 17:56:58 -07:00
|
|
|
return fastapi_app
|