Some checks failed
Run tests and pre-commit / Run tests and pre-commit hooks (push) Has been cancelled
Run tests and pre-commit / Frontend Lint and Build (push) Has been cancelled
Publish Fern Docs / run (push) Has been cancelled
Update OpenAPI Specification / update-openapi (push) Has been cancelled
- Implemented full Russian translation (ru) for 8 major pages - Added LanguageSwitcher component with language detection - Translated: Navigation, Settings, Workflows, Credentials, Banner, Examples - Fixed API endpoint path: changed to use sans-api-v1 client for /v1/ endpoints - Fixed CORS: added http://localhost:8081 to ALLOWED_ORIGINS - Added locales infrastructure with i18next and react-i18next - Created bilingual JSON files (en/ru) for 4 namespaces - 220+ translation keys implemented - Backend CORS configuration updated in .env - Documentation: I18N implementation guides and installation docs
206 lines
7.4 KiB
Python
206 lines
7.4 KiB
Python
import uuid
|
|
from contextlib import asynccontextmanager
|
|
from datetime import datetime
|
|
from typing import Any, AsyncGenerator, Awaitable, Callable
|
|
|
|
import structlog
|
|
from fastapi import FastAPI, Response, status
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.openapi.utils import get_openapi
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import ValidationError
|
|
from starlette.requests import HTTPConnection, Request
|
|
from starlette_context.middleware import RawContextMiddleware
|
|
from starlette_context.plugins.base import Plugin
|
|
|
|
from skyvern.config import settings
|
|
from skyvern.exceptions import SkyvernHTTPException
|
|
from skyvern.forge import app as forge_app
|
|
from skyvern.forge.forge_app_initializer import start_forge_app
|
|
from skyvern.forge.request_logging import log_raw_request_middleware
|
|
from skyvern.forge.sdk.core import skyvern_context
|
|
from skyvern.forge.sdk.core.skyvern_context import SkyvernContext
|
|
from skyvern.forge.sdk.db.exceptions import NotFoundError
|
|
from skyvern.forge.sdk.routes import internal_auth
|
|
from skyvern.forge.sdk.routes.routers import base_router, legacy_base_router, legacy_v2_router
|
|
from skyvern.services.cleanup_service import start_cleanup_scheduler, stop_cleanup_scheduler
|
|
|
|
try:
|
|
from cloud.observability.otel_setup import OTELSetup
|
|
except ImportError:
|
|
OTELSetup = None # type: ignore[assignment,misc]
|
|
|
|
LOG = structlog.get_logger()
|
|
|
|
|
|
class ExecutionDatePlugin(Plugin):
|
|
key = "execution_date"
|
|
|
|
async def process_request(self, request: Request | HTTPConnection) -> datetime:
|
|
return datetime.now()
|
|
|
|
|
|
def custom_openapi(app: FastAPI) -> dict:
|
|
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,
|
|
)
|
|
openapi_schema["servers"] = [
|
|
{"url": "https://api.skyvern.com", "x-fern-server-name": "Cloud"},
|
|
{"url": "https://api-staging.skyvern.com", "x-fern-server-name": "Staging"},
|
|
{"url": "http://localhost:8000", "x-fern-server-name": "Local"},
|
|
]
|
|
app.openapi_schema = openapi_schema
|
|
return app.openapi_schema
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(fastapi_app: FastAPI) -> AsyncGenerator[None, Any]:
|
|
"""Lifespan context manager for FastAPI app startup and shutdown."""
|
|
|
|
LOG.info("Server started")
|
|
|
|
# TEMPORARY FIX: Commented out to allow backend startup
|
|
# if forge_app.api_app_startup_event:
|
|
# LOG.info("Calling api app startup event")
|
|
# try:
|
|
# await forge_app.api_app_startup_event(fastapi_app)
|
|
# except Exception:
|
|
# LOG.exception("Failed to execute api app startup event")
|
|
|
|
# Start cleanup scheduler if enabled
|
|
cleanup_task = start_cleanup_scheduler()
|
|
if cleanup_task:
|
|
LOG.info("Cleanup scheduler started")
|
|
|
|
yield
|
|
|
|
# Stop cleanup scheduler
|
|
await stop_cleanup_scheduler()
|
|
|
|
# Close notification registry (e.g. cancel Redis listener tasks)
|
|
from skyvern.forge.sdk.notification.factory import NotificationRegistryFactory
|
|
|
|
registry = NotificationRegistryFactory.get_registry()
|
|
if hasattr(registry, "close"):
|
|
await registry.close()
|
|
|
|
# Close shared Redis client (after registry so listener tasks drain first)
|
|
from skyvern.forge.sdk.redis.factory import RedisClientFactory
|
|
|
|
redis_client = RedisClientFactory.get_client()
|
|
if redis_client is not None:
|
|
await redis_client.close()
|
|
|
|
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")
|
|
LOG.info("Server shutting down")
|
|
|
|
|
|
def create_api_app() -> FastAPI:
|
|
"""
|
|
Start the agent server.
|
|
"""
|
|
# 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))
|
|
|
|
forge_app_instance = start_forge_app()
|
|
|
|
fastapi_app = FastAPI(lifespan=lifespan)
|
|
|
|
# Add CORS middleware
|
|
fastapi_app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.ALLOWED_ORIGINS,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
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")
|
|
|
|
# local dev endpoints
|
|
if settings.ENV == "local":
|
|
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")
|
|
|
|
fastapi_app.openapi = lambda: custom_openapi(fastapi_app)
|
|
|
|
fastapi_app.add_middleware(
|
|
RawContextMiddleware,
|
|
plugins=(
|
|
# TODO (suchintan): We should set these up
|
|
ExecutionDatePlugin(),
|
|
# RequestIdPlugin(),
|
|
# UserAgentPlugin(),
|
|
),
|
|
)
|
|
|
|
@fastapi_app.exception_handler(NotFoundError)
|
|
async def handle_not_found_error(request: Request, exc: NotFoundError) -> Response:
|
|
return Response(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
@fastapi_app.exception_handler(SkyvernHTTPException)
|
|
async def handle_skyvern_http_exception(request: Request, exc: SkyvernHTTPException) -> JSONResponse:
|
|
return JSONResponse(status_code=exc.status_code, content={"detail": exc.message})
|
|
|
|
@fastapi_app.exception_handler(ValidationError)
|
|
async def handle_pydantic_validation_error(request: Request, exc: ValidationError) -> JSONResponse:
|
|
return JSONResponse(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
content={"detail": str(exc)},
|
|
)
|
|
|
|
@fastapi_app.exception_handler(Exception)
|
|
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}"})
|
|
|
|
@fastapi_app.middleware("http")
|
|
async def request_middleware(request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
|
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())
|
|
|
|
try:
|
|
return await call_next(request)
|
|
finally:
|
|
skyvern_context.reset()
|
|
|
|
@fastapi_app.middleware("http")
|
|
async def raw_request_logging(request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
|
return await log_raw_request_middleware(request, call_next)
|
|
|
|
if forge_app_instance.setup_api_app:
|
|
forge_app_instance.setup_api_app(fastapi_app)
|
|
|
|
return fastapi_app
|
|
|
|
|
|
# Create app instance for uvicorn
|
|
app = create_api_app()
|
|
|