|
| 1 | +"""Service container — constructs all application services and wires their dependencies. |
| 2 | +
|
| 3 | +Returns a dict consumed by routes (via FastAPI Depends) and the lifespan hook. |
| 4 | +""" |
| 5 | +import os |
| 6 | + |
| 7 | +from api.connector_router import ConnectorRouter |
| 8 | +from config.settings import ( |
| 9 | + INGESTION_TIMEOUT, |
| 10 | + SESSION_SECRET, |
| 11 | + clients, |
| 12 | + config_manager, |
| 13 | + get_embedding_model, |
| 14 | + get_index_name, |
| 15 | + is_no_auth_mode, |
| 16 | +) |
| 17 | +from connectors.langflow_connector_service import LangflowConnectorService |
| 18 | +from connectors.service import ConnectorService |
| 19 | +from services.api_key_service import APIKeyService |
| 20 | +from services.auth_service import AuthService |
| 21 | +from services.chat_service import ChatService |
| 22 | +from services.document_service import DocumentService |
| 23 | +from services.flows_service import FlowsService |
| 24 | +from services.knowledge_filter_service import KnowledgeFilterService |
| 25 | +from services.langflow_file_service import LangflowFileService |
| 26 | +from services.langflow_mcp_service import LangflowMCPService |
| 27 | +from services.models_service import ModelsService |
| 28 | +from services.monitor_service import MonitorService |
| 29 | +from services.search_service import SearchService, register_search_service |
| 30 | +from services.task_service import TaskService |
| 31 | +from session_manager import SessionManager |
| 32 | +from utils.jwt_keygen import generate_jwt_keys |
| 33 | +from utils.logging_config import get_logger |
| 34 | +from utils.telemetry import TelemetryClient, Category, MessageId |
| 35 | + |
| 36 | +logger = get_logger(__name__) |
| 37 | + |
| 38 | + |
| 39 | +async def initialize_services(): |
| 40 | + """Initialize all services and their dependencies""" |
| 41 | + await TelemetryClient.send_event( |
| 42 | + Category.SERVICE_INITIALIZATION, MessageId.ORB_SVC_INIT_START |
| 43 | + ) |
| 44 | + from config.settings import IBM_AUTH_ENABLED |
| 45 | + |
| 46 | + if IBM_AUTH_ENABLED: |
| 47 | + logger.info("IBM auth mode enabled — JWT validation delegated to Traefik") |
| 48 | + |
| 49 | + # Generate JWT keys if they don't exist, a JWT signing key isn't specified, |
| 50 | + # and IBM auth is not enabled (IBM mode delegates all auth to Traefik) |
| 51 | + if not os.getenv("JWT_SIGNING_KEY") and not IBM_AUTH_ENABLED: |
| 52 | + generate_jwt_keys() |
| 53 | + |
| 54 | + # Initialize clients (now async to generate Langflow API key) |
| 55 | + try: |
| 56 | + await clients.initialize() |
| 57 | + except Exception as e: |
| 58 | + logger.error("Failed to initialize clients", error=str(e)) |
| 59 | + await TelemetryClient.send_event( |
| 60 | + Category.SERVICE_INITIALIZATION, MessageId.ORB_SVC_OS_CLIENT_FAIL |
| 61 | + ) |
| 62 | + raise |
| 63 | + |
| 64 | + session_manager = SessionManager(SESSION_SECRET) |
| 65 | + |
| 66 | + models_service = ModelsService() |
| 67 | + document_service = DocumentService( |
| 68 | + session_manager=session_manager, |
| 69 | + models_service=models_service, |
| 70 | + docling_service=clients.docling_service, |
| 71 | + ) |
| 72 | + search_service = SearchService(session_manager, models_service) |
| 73 | + register_search_service(search_service) |
| 74 | + task_service = TaskService( |
| 75 | + document_service, |
| 76 | + models_service, |
| 77 | + ingestion_timeout=INGESTION_TIMEOUT, |
| 78 | + docling_service=clients.docling_service, |
| 79 | + ) |
| 80 | + flows_service = FlowsService() |
| 81 | + chat_service = ChatService(flows_service=flows_service) |
| 82 | + knowledge_filter_service = KnowledgeFilterService(session_manager) |
| 83 | + monitor_service = MonitorService(session_manager) |
| 84 | + langflow_file_service = LangflowFileService( |
| 85 | + flows_service=flows_service, |
| 86 | + docling_service=clients.docling_service, |
| 87 | + ) |
| 88 | + langflow_mcp_service = LangflowMCPService() |
| 89 | + |
| 90 | + langflow_connector_service = LangflowConnectorService( |
| 91 | + task_service=task_service, |
| 92 | + session_manager=session_manager, |
| 93 | + flows_service=flows_service, |
| 94 | + docling_service=clients.docling_service, |
| 95 | + ) |
| 96 | + openrag_connector_service = ConnectorService( |
| 97 | + patched_async_client=clients, |
| 98 | + embed_model=get_embedding_model(), |
| 99 | + index_name=get_index_name(), |
| 100 | + task_service=task_service, |
| 101 | + session_manager=session_manager, |
| 102 | + models_service=models_service, |
| 103 | + document_service=document_service, |
| 104 | + docling_service=clients.docling_service, |
| 105 | + ) |
| 106 | + |
| 107 | + connector_service = ConnectorRouter( |
| 108 | + langflow_connector_service=langflow_connector_service, |
| 109 | + openrag_connector_service=openrag_connector_service, |
| 110 | + ) |
| 111 | + |
| 112 | + auth_service = AuthService( |
| 113 | + session_manager, |
| 114 | + connector_service, |
| 115 | + flows_service, |
| 116 | + langflow_mcp_service=langflow_mcp_service, |
| 117 | + ) |
| 118 | + |
| 119 | + # Load persisted connector connections at startup so webhooks and syncs |
| 120 | + # can resolve existing subscriptions immediately after server boot |
| 121 | + # Skip in no-auth mode since connectors require OAuth |
| 122 | + if not is_no_auth_mode(): |
| 123 | + try: |
| 124 | + await connector_service.initialize() |
| 125 | + loaded_count = len(connector_service.connection_manager.connections) |
| 126 | + logger.info( |
| 127 | + "Loaded persisted connector connections on startup", |
| 128 | + loaded_count=loaded_count, |
| 129 | + ) |
| 130 | + except Exception as e: |
| 131 | + logger.error( |
| 132 | + "Failed to load persisted connections on startup", error=str(e) |
| 133 | + ) |
| 134 | + await TelemetryClient.send_event( |
| 135 | + Category.CONNECTOR_OPERATIONS, MessageId.ORB_CONN_LOAD_FAILED |
| 136 | + ) |
| 137 | + else: |
| 138 | + logger.info("[CONNECTORS] Skipping connection loading in no-auth mode") |
| 139 | + |
| 140 | + await TelemetryClient.send_event( |
| 141 | + Category.SERVICE_INITIALIZATION, MessageId.ORB_SVC_INIT_SUCCESS |
| 142 | + ) |
| 143 | + |
| 144 | + api_key_service = APIKeyService(session_manager) |
| 145 | + |
| 146 | + # ===== RBAC service ===== |
| 147 | + # We do NOT open the SQL engine here. `create_app()` runs inside an |
| 148 | + # `asyncio.run(...)` whose loop is closed BEFORE uvicorn starts its |
| 149 | + # own loop — any AsyncEngine bound to this loop would be dead by |
| 150 | + # then. Instead, RBACService takes a *lazy* session-factory getter |
| 151 | + # that resolves `db.engine.SessionLocal` at call time — that |
| 152 | + # attribute is filled in by the lifespan startup event running on |
| 153 | + # uvicorn's loop. Alembic upgrade is run synchronously from __main__ |
| 154 | + # before `asyncio.run(create_app())` so the schema is in place by |
| 155 | + # the time the lifespan opens the engine. |
| 156 | + from db import engine as _db_engine_mod |
| 157 | + from services.rbac_service import RBACService |
| 158 | + from services.workspace_config_service import WorkspaceConfigService |
| 159 | + |
| 160 | + def _lazy_session_factory(): |
| 161 | + sl = _db_engine_mod.SessionLocal |
| 162 | + if sl is None: |
| 163 | + raise RuntimeError( |
| 164 | + "DB engine not yet initialized. RBACService called before lifespan startup." |
| 165 | + ) |
| 166 | + return sl() |
| 167 | + |
| 168 | + rbac_service = RBACService(_lazy_session_factory) |
| 169 | + |
| 170 | + # WorkspaceConfigService — DB-first reads of what config.yaml holds, |
| 171 | + # with the legacy ConfigManager kept as the yaml fallback during |
| 172 | + # Phase B (dual-write). |
| 173 | + workspace_config_service = WorkspaceConfigService( |
| 174 | + config_manager=config_manager, |
| 175 | + session_factory=_lazy_session_factory, |
| 176 | + ) |
| 177 | + |
| 178 | + # Plumb the session factory into the two chat-history services |
| 179 | + # (session_ownership + conversation_persistence). They lazy-resolve |
| 180 | + # `db.engine.SessionLocal` as a fallback, but setting it here makes |
| 181 | + # the wiring explicit and avoids the import path on the hot loop. |
| 182 | + from services.session_ownership_service import session_ownership_service |
| 183 | + from services.conversation_persistence_service import conversation_persistence |
| 184 | + session_ownership_service._session_factory = _lazy_session_factory |
| 185 | + conversation_persistence._session_factory = _lazy_session_factory |
| 186 | + |
| 187 | + return { |
| 188 | + "document_service": document_service, |
| 189 | + "search_service": search_service, |
| 190 | + "task_service": task_service, |
| 191 | + "chat_service": chat_service, |
| 192 | + "flows_service": flows_service, |
| 193 | + "langflow_file_service": langflow_file_service, |
| 194 | + "auth_service": auth_service, |
| 195 | + "connector_service": connector_service, |
| 196 | + "knowledge_filter_service": knowledge_filter_service, |
| 197 | + "models_service": models_service, |
| 198 | + "monitor_service": monitor_service, |
| 199 | + "session_manager": session_manager, |
| 200 | + "api_key_service": api_key_service, |
| 201 | + "langflow_mcp_service": langflow_mcp_service, |
| 202 | + "docling_service": clients.docling_service, |
| 203 | + "rbac_service": rbac_service, |
| 204 | + "workspace_config_service": workspace_config_service, |
| 205 | + } |
0 commit comments