mirror of
https://github.com/langgenius/dify.git
synced 2026-11-10 21:35:45 +08:00
192 lines
8.7 KiB
Python
192 lines
8.7 KiB
Python
"""FastAPI application factory for the Dify Agent run server.
|
|
|
|
The HTTP process owns Redis clients plus separate shared ``httpx.AsyncClient``
|
|
instances for plugin-daemon and Dify API inner calls, route wiring, and a
|
|
process-local scheduler. Run execution happens in background ``asyncio`` tasks
|
|
rather than request handlers, so client disconnects do not cancel the agent
|
|
runtime. Redis persists run records and per-run event streams with configured
|
|
retention only; it is not used as a job queue. Agenton layers and providers
|
|
stay state-only: they borrow the lifespan-owned clients through the runner and
|
|
receive runtime-backend and Shell settings through provider construction rather
|
|
than reading environment variables themselves. The standard server mounts the
|
|
HTTP Agent Stub router. Process-level Logfire instrumentation is configured at
|
|
app construction time and only exports remotely when Logfire's default
|
|
environment configuration provides a token.
|
|
"""
|
|
|
|
from collections.abc import AsyncGenerator
|
|
from contextlib import asynccontextmanager
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, FastAPI
|
|
from redis.asyncio import Redis
|
|
|
|
from dify_agent.agent_stub.shell_env import ShellAgentStubTokenFactory
|
|
from dify_agent.agent_stub.server.router import create_agent_stub_router
|
|
from dify_agent.layers.execution_context import DifyExecutionContextLayerConfig
|
|
from dify_agent.runtime.compositor_factory import create_default_layer_providers
|
|
from dify_agent.runtime.run_scheduler import RunScheduler
|
|
from dify_agent.server.auth import create_bearer_token_dependency
|
|
from dify_agent.server.observability import configure_server_observability
|
|
from dify_agent.server.routes.runs import create_runs_router
|
|
from dify_agent.server.routes.execution_bindings import create_execution_bindings_router
|
|
from dify_agent.server.routes.home_snapshots import create_home_snapshots_router
|
|
from dify_agent.server.routes.binding_files import create_binding_files_router
|
|
from dify_agent.server.execution_bindings import ExecutionBindingService
|
|
from dify_agent.server.binding_files import BindingFileService
|
|
from dify_agent.server.home_snapshots import HomeSnapshotService
|
|
from dify_agent.server.settings import ServerSettings
|
|
from dify_agent.storage.redis_run_store import RedisRunStore
|
|
|
|
|
|
def create_app(settings: ServerSettings | None = None) -> FastAPI:
|
|
"""Build the FastAPI app with one shared Redis store and local scheduler."""
|
|
resolved_settings = settings or ServerSettings()
|
|
agent_stub_token_codec = resolved_settings.create_agent_stub_token_codec()
|
|
agent_stub_token_factory: ShellAgentStubTokenFactory | None = None
|
|
if agent_stub_token_codec is not None:
|
|
# Runtime receives only this callable boundary; the HTTP router keeps
|
|
# the concrete token codec on the server side.
|
|
def issue_agent_stub_token(
|
|
execution_context: DifyExecutionContextLayerConfig,
|
|
*,
|
|
session_id: str | None,
|
|
) -> str:
|
|
return agent_stub_token_codec.encode_connection_token(
|
|
execution_context,
|
|
session_id=session_id,
|
|
)
|
|
|
|
agent_stub_token_factory = issue_agent_stub_token
|
|
agent_stub_file_request_handler = resolved_settings.create_agent_stub_file_request_handler()
|
|
agent_stub_config_request_handler = resolved_settings.create_agent_stub_config_request_handler()
|
|
agent_stub_drive_request_handler = resolved_settings.create_agent_stub_drive_request_handler()
|
|
runtime_backend_profile = resolved_settings.build_runtime_backend_profile()
|
|
layer_providers = create_default_layer_providers(
|
|
plugin_daemon_url=resolved_settings.plugin_daemon_url,
|
|
plugin_daemon_api_key=resolved_settings.plugin_daemon_api_key,
|
|
inner_api_url=resolved_settings.inner_api_url,
|
|
inner_api_key=resolved_settings.inner_api_key or "",
|
|
runtime_backend_profile=runtime_backend_profile,
|
|
shell_redact_patterns=resolved_settings.get_shell_redact_patterns(),
|
|
agent_stub_api_base_url=resolved_settings.agent_stub_api_base_url,
|
|
agent_stub_token_factory=agent_stub_token_factory,
|
|
)
|
|
binding_file_service = (
|
|
BindingFileService(
|
|
execution_bindings=runtime_backend_profile.execution_bindings,
|
|
agent_stub_api_base_url=resolved_settings.agent_stub_api_base_url,
|
|
agent_stub_token_factory=agent_stub_token_factory,
|
|
)
|
|
if runtime_backend_profile is not None
|
|
else None
|
|
)
|
|
home_snapshot_service = (
|
|
HomeSnapshotService(
|
|
home_snapshots=runtime_backend_profile.home_snapshots,
|
|
execution_bindings=runtime_backend_profile.execution_bindings,
|
|
)
|
|
if runtime_backend_profile is not None
|
|
else None
|
|
)
|
|
execution_binding_service = (
|
|
ExecutionBindingService(backend=runtime_backend_profile.execution_bindings)
|
|
if runtime_backend_profile is not None
|
|
else None
|
|
)
|
|
state: dict[str, object] = {}
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(_app: FastAPI) -> AsyncGenerator[None, None]:
|
|
redis = Redis.from_url(resolved_settings.redis_url)
|
|
plugin_daemon_http_client = create_plugin_daemon_http_client(resolved_settings)
|
|
dify_api_inner_http_client = create_dify_api_inner_http_client(resolved_settings)
|
|
store = RedisRunStore(
|
|
redis,
|
|
prefix=resolved_settings.redis_prefix,
|
|
run_retention_seconds=resolved_settings.run_retention_seconds,
|
|
)
|
|
scheduler = RunScheduler(
|
|
store=store,
|
|
plugin_daemon_http_client=plugin_daemon_http_client,
|
|
dify_api_http_client=dify_api_inner_http_client,
|
|
shutdown_grace_seconds=resolved_settings.shutdown_grace_seconds,
|
|
layer_providers=layer_providers,
|
|
)
|
|
state["store"] = store
|
|
state["scheduler"] = scheduler
|
|
try:
|
|
yield
|
|
finally:
|
|
await scheduler.shutdown()
|
|
await dify_api_inner_http_client.aclose()
|
|
await plugin_daemon_http_client.aclose()
|
|
await redis.aclose()
|
|
|
|
app = FastAPI(title="Dify Agent Run Server", version="0.1.0", lifespan=lifespan)
|
|
configure_server_observability(app)
|
|
|
|
def get_store() -> RedisRunStore:
|
|
return state["store"] # pyright: ignore[reportReturnType]
|
|
|
|
def get_scheduler() -> RunScheduler:
|
|
return state["scheduler"] # pyright: ignore[reportReturnType]
|
|
|
|
control_plane_router = APIRouter(
|
|
dependencies=[create_bearer_token_dependency(resolved_settings.api_token)],
|
|
)
|
|
control_plane_router.include_router(create_runs_router(get_store, get_scheduler))
|
|
control_plane_router.include_router(create_execution_bindings_router(lambda: execution_binding_service))
|
|
control_plane_router.include_router(create_home_snapshots_router(lambda: home_snapshot_service))
|
|
control_plane_router.include_router(create_binding_files_router(lambda: binding_file_service))
|
|
app.include_router(control_plane_router)
|
|
app.include_router(
|
|
create_agent_stub_router(
|
|
token_codec=agent_stub_token_codec,
|
|
file_request_handler=agent_stub_file_request_handler,
|
|
config_request_handler=agent_stub_config_request_handler,
|
|
drive_request_handler=agent_stub_drive_request_handler,
|
|
)
|
|
)
|
|
return app
|
|
|
|
|
|
def create_plugin_daemon_http_client(settings: ServerSettings) -> httpx.AsyncClient:
|
|
"""Create the lifespan-owned plugin daemon HTTP client with configured limits.
|
|
|
|
The returned client is shared by all local background runs in this FastAPI
|
|
process and must be closed by the app lifespan after the scheduler has stopped
|
|
using it.
|
|
"""
|
|
return _create_shared_http_client(settings)
|
|
|
|
|
|
def create_dify_api_inner_http_client(settings: ServerSettings) -> httpx.AsyncClient:
|
|
"""Create the lifespan-owned Dify API inner HTTP client.
|
|
|
|
The Dify API inner client intentionally shares the generic outbound HTTP
|
|
timeout and connection-pool settings with the plugin daemon client so
|
|
operational tuning stays in one place while endpoint URL/API keys remain
|
|
distinct server settings.
|
|
"""
|
|
return _create_shared_http_client(settings)
|
|
|
|
|
|
def _create_shared_http_client(settings: ServerSettings) -> httpx.AsyncClient:
|
|
"""Build one shared HTTP client using generic outbound timeout/pool settings."""
|
|
return httpx.AsyncClient(
|
|
timeout=settings.create_outbound_http_timeout(),
|
|
limits=httpx.Limits(
|
|
max_connections=settings.outbound_http_max_connections,
|
|
max_keepalive_connections=settings.outbound_http_max_keepalive_connections,
|
|
keepalive_expiry=settings.outbound_http_keepalive_expiry,
|
|
),
|
|
trust_env=False,
|
|
)
|
|
|
|
|
|
app = create_app()
|
|
|
|
|
|
__all__ = ["app", "create_app", "create_dify_api_inner_http_client", "create_plugin_daemon_http_client"]
|